ivcap_fastapi 0.2.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,74 @@
1
+ Metadata-Version: 2.1
2
+ Name: ivcap_fastapi
3
+ Version: 0.2.0
4
+ Summary: Helper functions for building FastAPI based IVCAP services
5
+ Author: Max Ott
6
+ Author-email: max.ott@csiro.au
7
+ Classifier: Programming Language :: Python :: 2
8
+ Classifier: Programming Language :: Python :: 2.7
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.4
11
+ Classifier: Programming Language :: Python :: 3.5
12
+ Classifier: Programming Language :: Python :: 3.6
13
+ Classifier: Programming Language :: Python :: 3.7
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Description-Content-Type: text/markdown
20
+
21
+ # ivcap_fastapi: Python helpers for building FastAPI based IVCAP services
22
+
23
+ A python library containing various helper and middleware functions
24
+ to support converting FastAPI based tools into IVCAP services.
25
+
26
+ ## Content
27
+
28
+ * [Try-Later Middleware](#try-later)
29
+ * [JSON-RPC Middleware](#json-rpc)
30
+
31
+ ### Try-Later Middleware <a name="try-later"></a>
32
+
33
+ This middleware is supporting the use case where the execution of a
34
+ requested service is taking longer than the caller is willing to wait.
35
+ A typical use case is where the service is itself outsourcing the execution
36
+ to some other long-running service but may immediately receive a reference
37
+ to the eventual result.
38
+
39
+ In this case, raising a `TryLaterException` will return with a 204
40
+ status code and additional information on how to later check back for the
41
+ result.
42
+
43
+ ```python
44
+ from ivcap_fastapi import TryLaterException, use_try_later_middleware
45
+ use_try_later_middleware(app)
46
+
47
+ @app.post("/big_job")
48
+ def big_job(req: Request) -> Response:
49
+ jobID, expected_exec_time = scheduling_big_job(req)
50
+ raise TryLaterException(f"/jobs/{jobID}", expected_exec_time)
51
+
52
+ @app.get("/jobs/{jobID}")
53
+ def get_job(jobID: str) -> Response:
54
+ resp = find_result_for(job_id)
55
+ return resp
56
+ ```
57
+
58
+ Specifically, raising `TryLaterException(location, delay)` will
59
+ return an HTTP response with a 204 status code with the additional
60
+ HTTP headers `Location` and `Retry-Later` set to `location` and
61
+ `delay` respectively.
62
+
63
+ ### JSON-RPC Middleware <a name="json-rpc"></a>
64
+
65
+ This middleware will convert any `POST /` with a payload
66
+ following the [JSON-RPC](https://www.jsonrpc.org/specification)
67
+ specification to an internal `POST /{method}` and will return
68
+ the result formatted according to the JSON-RPC spec.
69
+
70
+ ```python
71
+ from ivcap_fastapi import use_json_rpc_middleware
72
+ use_json_rpc_middleware(app)
73
+ ```
74
+
@@ -0,0 +1,53 @@
1
+ # ivcap_fastapi: Python helpers for building FastAPI based IVCAP services
2
+
3
+ A python library containing various helper and middleware functions
4
+ to support converting FastAPI based tools into IVCAP services.
5
+
6
+ ## Content
7
+
8
+ * [Try-Later Middleware](#try-later)
9
+ * [JSON-RPC Middleware](#json-rpc)
10
+
11
+ ### Try-Later Middleware <a name="try-later"></a>
12
+
13
+ This middleware is supporting the use case where the execution of a
14
+ requested service is taking longer than the caller is willing to wait.
15
+ A typical use case is where the service is itself outsourcing the execution
16
+ to some other long-running service but may immediately receive a reference
17
+ to the eventual result.
18
+
19
+ In this case, raising a `TryLaterException` will return with a 204
20
+ status code and additional information on how to later check back for the
21
+ result.
22
+
23
+ ```python
24
+ from ivcap_fastapi import TryLaterException, use_try_later_middleware
25
+ use_try_later_middleware(app)
26
+
27
+ @app.post("/big_job")
28
+ def big_job(req: Request) -> Response:
29
+ jobID, expected_exec_time = scheduling_big_job(req)
30
+ raise TryLaterException(f"/jobs/{jobID}", expected_exec_time)
31
+
32
+ @app.get("/jobs/{jobID}")
33
+ def get_job(jobID: str) -> Response:
34
+ resp = find_result_for(job_id)
35
+ return resp
36
+ ```
37
+
38
+ Specifically, raising `TryLaterException(location, delay)` will
39
+ return an HTTP response with a 204 status code with the additional
40
+ HTTP headers `Location` and `Retry-Later` set to `location` and
41
+ `delay` respectively.
42
+
43
+ ### JSON-RPC Middleware <a name="json-rpc"></a>
44
+
45
+ This middleware will convert any `POST /` with a payload
46
+ following the [JSON-RPC](https://www.jsonrpc.org/specification)
47
+ specification to an internal `POST /{method}` and will return
48
+ the result formatted according to the JSON-RPC spec.
49
+
50
+ ```python
51
+ from ivcap_fastapi import use_json_rpc_middleware
52
+ use_json_rpc_middleware(app)
53
+ ```
@@ -0,0 +1,39 @@
1
+ [tool.poetry]
2
+ name = "ivcap_fastapi"
3
+ version = "0.2.0"
4
+ description = "Helper functions for building FastAPI based IVCAP services"
5
+
6
+ authors = ["Max Ott <max.ott@csiro.au>"]
7
+
8
+ readme = "README.md"
9
+
10
+ include = ["src/ivcap_fastapi/py.typed"]
11
+
12
+ [tool.poetry.dependencies]
13
+
14
+ [tool.poetry.dev-dependencies]
15
+
16
+ [tool.poetry.group.dev.dependencies]
17
+ fastapi = { version = "^0.111.1", extras = ["standard"] }
18
+ pytest = "^7.1.3"
19
+ pytest-cov = "^4.1.0"
20
+ Sphinx = "^5.2.3"
21
+ myst-nb = "^0.17.1"
22
+ autoapi = "^2.0.1"
23
+ sphinx-autoapi = "^2.0.0"
24
+ sphinx-rtd-theme = "^1.0.0"
25
+ licenseheaders = "^0.8.8"
26
+
27
+ [tool.semantic_release]
28
+ version_variable = "pyproject.toml:version" # version location
29
+ branch = "main" # branch to make releases of
30
+ build_command = "poetry build" # build dists
31
+ dist_path = "dist/" # where to put dists
32
+ upload_to_release = true # auto-create GitHub release
33
+ upload_to_pypi = false # don't auto-upload to PyPI
34
+ remove_dist = false # don't remove dists
35
+ patch_without_tag = true # patch release by default
36
+
37
+ [build-system]
38
+ requires = ["poetry-core>=1.0.0"]
39
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,20 @@
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 client library for accessing IVCAP """
7
+
8
+ # read version from installed package
9
+ try: # Python < 3.10 (backport)
10
+ from importlib_metadata import version
11
+ except ImportError:
12
+ from importlib.metadata import version
13
+ try:
14
+ __version__ = version("ivcap_client")
15
+ except Exception:
16
+ __version__ = "unknown" # should only happen when running the local examples
17
+
18
+ from .json_rpc import use_json_rpc_middleware
19
+ from .try_later import TryLaterException, use_try_later_middleware
20
+ from .logger import getLogger, service_log_config, logging_init
@@ -0,0 +1,120 @@
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
+
8
+ from starlette.datastructures import Headers
9
+ from starlette.responses import Response
10
+ from starlette.requests import ClientDisconnect
11
+ from starlette.types import ASGIApp, Receive, Scope, Send
12
+
13
+ class JsonRPCMiddleware:
14
+ def __init__(self, app: ASGIApp) -> None:
15
+ self.app = app
16
+
17
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
18
+ if scope["type"] != "http":
19
+ await self.app(scope, receive, send)
20
+ return
21
+
22
+ if scope["path"] == "/":
23
+ headers = Headers(scope=scope)
24
+ ct = headers.get("Content-Type", "")
25
+ if scope["method"] == "POST" and ct == "application/json":
26
+ await self.call_rpc(scope, receive, send)
27
+ return
28
+
29
+ await self.app(scope, receive, send)
30
+
31
+ async def call_rpc(self, scope: Scope, receive: Receive, send: Send) -> None:
32
+ message = await self.get_full_message(receive)
33
+ async def mod_receive():
34
+ nonlocal message
35
+ return message
36
+
37
+ s = message["body"].decode('utf-8')
38
+ pyld = json.loads(s)
39
+ if pyld.get("jsonrpc") != "2.0":
40
+ await self.app(scope, mod_receive, send)
41
+ return
42
+
43
+ method = pyld.get("method")
44
+ params = pyld.get("params")
45
+ id = pyld.get("id")
46
+ if method == None or params == None:
47
+ response = Response(status_code=400)
48
+ await response(scope, receive, send)
49
+ return
50
+
51
+ scope["path"] = "/" + method
52
+ ps = json.dumps(params)
53
+ message["body"] = ps.encode('utf-8')
54
+
55
+
56
+ startMsg = None
57
+ async def mod_send(message):
58
+ if message["type"] == "http.response.start":
59
+ nonlocal startMsg
60
+ startMsg = message # fix content-length later
61
+
62
+ elif message["type"] == "http.response.body":
63
+ body = self.create_reply(message["body"], id)
64
+ message["body"] = body
65
+ startMsg["headers"] = self.set_content_length(len(body), startMsg["headers"])
66
+
67
+ await send(message)
68
+
69
+ await self.app(scope, mod_receive, mod_send)
70
+
71
+ def create_reply(self, body, id):
72
+ #{"jsonrpc": "2.0", "result": 19, "id": 3}
73
+ s = body.decode('utf-8')
74
+ result = json.loads(s)
75
+ reply = {
76
+ "jsonrpc": "2.0",
77
+ "result": result,
78
+ "id": id,
79
+ }
80
+ return json.dumps(reply).encode('utf-8')
81
+
82
+ def set_content_length(self, length, headers):
83
+ def f(t):
84
+ k = t[0].decode('utf-8')
85
+ if k == "content-length":
86
+ return (t[0], f"{length}".encode('utf-8'))
87
+ return t
88
+
89
+ l = list(map(f, headers))
90
+ return l
91
+
92
+ async def get_full_message(self, receive: Receive):
93
+ message = None
94
+ chunks: list[bytes] = []
95
+ done = False
96
+
97
+ async def g():
98
+ nonlocal message, chunks, done
99
+ while not done:
100
+ m = await receive()
101
+ if m["type"] == "http.request":
102
+ if message == None:
103
+ message = m
104
+ body = m.get("body", b"")
105
+ chunks.append(body)
106
+ if not m.get("more_body", False):
107
+ message[body] = chunks
108
+ done = True
109
+ yield message
110
+ elif message["type"] == "http.disconnect":
111
+ raise ClientDisconnect()
112
+ yield None
113
+
114
+ async for _ in g():
115
+ pass
116
+
117
+ return message
118
+
119
+ def use_json_rpc_middleware(app):
120
+ app.add_middleware(JsonRPCMiddleware)
@@ -0,0 +1,26 @@
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
+ LOGGING_CONFIG = json.load(file)
26
+ 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
@@ -0,0 +1,29 @@
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 fastapi import Response, Request
7
+
8
+ class TryLaterException(Exception):
9
+ """An exception to raise if a computation will take longer and the result
10
+ should be collected later at a different method."""
11
+ def __init__(self, location, wait_time):
12
+ super().__init__(location)
13
+ self.location = location
14
+ self.wait_time = wait_time
15
+
16
+ def response(self):
17
+ r = Response(status_code=204) # No Content
18
+ r.headers["Location"] = self.location
19
+ r.headers["Retry-Later"] = f"{self.wait_time}"
20
+ return r
21
+
22
+ async def _try_later(request: Request, call_next) -> Response:
23
+ try:
24
+ return await call_next(request)
25
+ except TryLaterException as e:
26
+ return e.response()
27
+
28
+ def use_try_later_middleware(app):
29
+ app.middleware("http")(_try_later)