ivcap_fastapi 0.2.0__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
@@ -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)