twirpy 0.1.0.dev3__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.
- twirpy-0.1.0.dev3/.gitignore +2 -0
- twirpy-0.1.0.dev3/LICENSE +28 -0
- twirpy-0.1.0.dev3/PKG-INFO +81 -0
- twirpy-0.1.0.dev3/README.md +65 -0
- twirpy-0.1.0.dev3/pyproject.toml +62 -0
- twirpy-0.1.0.dev3/twirp/__init__.py +1 -0
- twirpy-0.1.0.dev3/twirp/asgi.py +178 -0
- twirpy-0.1.0.dev3/twirp/async_client.py +50 -0
- twirpy-0.1.0.dev3/twirp/base.py +108 -0
- twirpy-0.1.0.dev3/twirp/client.py +44 -0
- twirpy-0.1.0.dev3/twirp/context.py +78 -0
- twirpy-0.1.0.dev3/twirp/ctxkeys.py +9 -0
- twirpy-0.1.0.dev3/twirp/errors.py +47 -0
- twirpy-0.1.0.dev3/twirp/exceptions.py +96 -0
- twirpy-0.1.0.dev3/twirp/hook.py +47 -0
- twirpy-0.1.0.dev3/twirp/logging.py +62 -0
- twirpy-0.1.0.dev3/twirp/server.py +32 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright 2024, [pafin Inc.](https://www.pafin.com)
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: twirpy
|
|
3
|
+
Version: 0.1.0.dev3
|
|
4
|
+
Summary: Twirp runtime library for Python
|
|
5
|
+
Project-URL: repository, https://github.com/cryptact/twirpy
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: protobuf,rpc,twirp
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Requires-Dist: protobuf
|
|
11
|
+
Requires-Dist: requests
|
|
12
|
+
Requires-Dist: structlog
|
|
13
|
+
Provides-Extra: async
|
|
14
|
+
Requires-Dist: aiohttp; extra == 'async'
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# Twirpy
|
|
18
|
+
|
|
19
|
+
> Twirp is a framework for service-to-service communication emphasizing simplicity and minimalism.
|
|
20
|
+
> It generates routing and serialization from API definition files and lets you focus on your application's logic
|
|
21
|
+
> instead of thinking about folderol like HTTP methods and paths and JSON.
|
|
22
|
+
>
|
|
23
|
+
> -- <cite>[Twirp's README](https://github.com/twitchtv/twirp/blob/main/README.md)</cite>
|
|
24
|
+
|
|
25
|
+
Twirpy is a Python implementation of the Twirp framework.
|
|
26
|
+
It currently supports [Twirp Wire Protocol v7](https://twitchtv.github.io/twirp/docs/spec_v7.html).
|
|
27
|
+
|
|
28
|
+
This repository contains:
|
|
29
|
+
* a protoc (aka the Protocol Compiler) plugin that generates sever and client code;
|
|
30
|
+
* a Python package with common implementation details.
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
### Runtime Library
|
|
35
|
+
|
|
36
|
+
The runtime library package contains common types like `TwirpServer` and `TwirpClient` that are used by the generated code.
|
|
37
|
+
|
|
38
|
+
Add the Twirp package to your Python project with:
|
|
39
|
+
```
|
|
40
|
+
pip install twirpy
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Code Generator
|
|
44
|
+
|
|
45
|
+
You need to install `protoc`, the Protocol Buffers compiler, and the `protoc-gen-twirpy` protoc plugin to generate code.
|
|
46
|
+
|
|
47
|
+
First, install the [Protocol Buffers](https://developers.google.com/protocol-buffers) compiler.
|
|
48
|
+
For installation instructions, see [Protocol Buffer Compiler Installation documentation](https://github.com/protocolbuffers/protobuf#protobuf-compiler-installation).
|
|
49
|
+
You can also use your package manager (e.g. `brew install protobuf` on macOS).
|
|
50
|
+
|
|
51
|
+
Go the [releases page](https://github.com/Cryptact/twirpy/releases/latest), and download the `protoc-gen-twirpy` binary for your platform.
|
|
52
|
+
Unzip the archive and move the binary to a directory in your PATH.
|
|
53
|
+
|
|
54
|
+
On macOS, you can use the following commands:
|
|
55
|
+
```sh
|
|
56
|
+
curl -L -o- \
|
|
57
|
+
https://github.com/Cryptact/twirpy/releases/latest/download/protoc-gen-twirpy_Darwin_arm64.tar.gz \
|
|
58
|
+
| tar xz -C ~/.local/bin protoc-gen-twirpy
|
|
59
|
+
````
|
|
60
|
+
|
|
61
|
+
## Generate and run
|
|
62
|
+
|
|
63
|
+
Use the protoc plugin to generate twirp server and client code.
|
|
64
|
+
```sh
|
|
65
|
+
protoc --python_out=. --pyi_out=. --twirpy_out=. example/rpc/haberdasher/service.proto
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
For more information on how to generate code, see the [example](example/README.md).
|
|
69
|
+
|
|
70
|
+
## Development
|
|
71
|
+
|
|
72
|
+
We use [`hatch`](https://hatch.pypa.io/latest/) to manage the development process.
|
|
73
|
+
|
|
74
|
+
To open a shell with the development environment, run: `hatch shell`.
|
|
75
|
+
To run the linter, run: `hatch fmt --check` or `hatch fmt` to fix the issues.
|
|
76
|
+
|
|
77
|
+
## Standing on the shoulders of giants
|
|
78
|
+
|
|
79
|
+
- The initial version of twirpy was made from an internal copy of https://github.com/daroot/protoc-gen-twirp_python_srv
|
|
80
|
+
- The work done by [Verloop](https://verloop.io/) on [the initial versions of Twirpy](https://github.com/verloop/twirpy).
|
|
81
|
+
- The `run_in_threadpool` method comes from https://github.com/encode/starlette
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Twirpy
|
|
2
|
+
|
|
3
|
+
> Twirp is a framework for service-to-service communication emphasizing simplicity and minimalism.
|
|
4
|
+
> It generates routing and serialization from API definition files and lets you focus on your application's logic
|
|
5
|
+
> instead of thinking about folderol like HTTP methods and paths and JSON.
|
|
6
|
+
>
|
|
7
|
+
> -- <cite>[Twirp's README](https://github.com/twitchtv/twirp/blob/main/README.md)</cite>
|
|
8
|
+
|
|
9
|
+
Twirpy is a Python implementation of the Twirp framework.
|
|
10
|
+
It currently supports [Twirp Wire Protocol v7](https://twitchtv.github.io/twirp/docs/spec_v7.html).
|
|
11
|
+
|
|
12
|
+
This repository contains:
|
|
13
|
+
* a protoc (aka the Protocol Compiler) plugin that generates sever and client code;
|
|
14
|
+
* a Python package with common implementation details.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
### Runtime Library
|
|
19
|
+
|
|
20
|
+
The runtime library package contains common types like `TwirpServer` and `TwirpClient` that are used by the generated code.
|
|
21
|
+
|
|
22
|
+
Add the Twirp package to your Python project with:
|
|
23
|
+
```
|
|
24
|
+
pip install twirpy
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Code Generator
|
|
28
|
+
|
|
29
|
+
You need to install `protoc`, the Protocol Buffers compiler, and the `protoc-gen-twirpy` protoc plugin to generate code.
|
|
30
|
+
|
|
31
|
+
First, install the [Protocol Buffers](https://developers.google.com/protocol-buffers) compiler.
|
|
32
|
+
For installation instructions, see [Protocol Buffer Compiler Installation documentation](https://github.com/protocolbuffers/protobuf#protobuf-compiler-installation).
|
|
33
|
+
You can also use your package manager (e.g. `brew install protobuf` on macOS).
|
|
34
|
+
|
|
35
|
+
Go the [releases page](https://github.com/Cryptact/twirpy/releases/latest), and download the `protoc-gen-twirpy` binary for your platform.
|
|
36
|
+
Unzip the archive and move the binary to a directory in your PATH.
|
|
37
|
+
|
|
38
|
+
On macOS, you can use the following commands:
|
|
39
|
+
```sh
|
|
40
|
+
curl -L -o- \
|
|
41
|
+
https://github.com/Cryptact/twirpy/releases/latest/download/protoc-gen-twirpy_Darwin_arm64.tar.gz \
|
|
42
|
+
| tar xz -C ~/.local/bin protoc-gen-twirpy
|
|
43
|
+
````
|
|
44
|
+
|
|
45
|
+
## Generate and run
|
|
46
|
+
|
|
47
|
+
Use the protoc plugin to generate twirp server and client code.
|
|
48
|
+
```sh
|
|
49
|
+
protoc --python_out=. --pyi_out=. --twirpy_out=. example/rpc/haberdasher/service.proto
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
For more information on how to generate code, see the [example](example/README.md).
|
|
53
|
+
|
|
54
|
+
## Development
|
|
55
|
+
|
|
56
|
+
We use [`hatch`](https://hatch.pypa.io/latest/) to manage the development process.
|
|
57
|
+
|
|
58
|
+
To open a shell with the development environment, run: `hatch shell`.
|
|
59
|
+
To run the linter, run: `hatch fmt --check` or `hatch fmt` to fix the issues.
|
|
60
|
+
|
|
61
|
+
## Standing on the shoulders of giants
|
|
62
|
+
|
|
63
|
+
- The initial version of twirpy was made from an internal copy of https://github.com/daroot/protoc-gen-twirp_python_srv
|
|
64
|
+
- The work done by [Verloop](https://verloop.io/) on [the initial versions of Twirpy](https://github.com/verloop/twirpy).
|
|
65
|
+
- The `run_in_threadpool` method comes from https://github.com/encode/starlette
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = [
|
|
3
|
+
"hatchling",
|
|
4
|
+
"hatch-semver"
|
|
5
|
+
]
|
|
6
|
+
build-backend = "hatchling.build"
|
|
7
|
+
|
|
8
|
+
[project]
|
|
9
|
+
name = "twirpy"
|
|
10
|
+
dynamic = ["version"]
|
|
11
|
+
description = "Twirp runtime library for Python"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
license = "BSD-3-Clause"
|
|
15
|
+
keywords = [
|
|
16
|
+
"protobuf",
|
|
17
|
+
"rpc",
|
|
18
|
+
"twirp",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"protobuf",
|
|
22
|
+
"requests",
|
|
23
|
+
"structlog",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
repository = "https://github.com/cryptact/twirpy"
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
async = [
|
|
31
|
+
"aiohttp",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[tool.hatch.version]
|
|
35
|
+
path = "twirp/__init__.py"
|
|
36
|
+
validate-bump = true
|
|
37
|
+
scheme = "semver"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.sdist]
|
|
40
|
+
include = ["/twirp"]
|
|
41
|
+
[tool.hatch.build.targets.wheel]
|
|
42
|
+
packages = ["twirp"]
|
|
43
|
+
|
|
44
|
+
[tool.hatch.envs.default]
|
|
45
|
+
python = "3.12"
|
|
46
|
+
dependencies = [
|
|
47
|
+
"aiohttp",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[tool.ruff]
|
|
51
|
+
line-length = 120
|
|
52
|
+
lint.select = [
|
|
53
|
+
"F", # pyflakes
|
|
54
|
+
"PLE", # pylint errors
|
|
55
|
+
"UP", # pyupgrade
|
|
56
|
+
]
|
|
57
|
+
lint.fixable = [
|
|
58
|
+
"F", # pyflakes
|
|
59
|
+
"PLE", # pylint errors
|
|
60
|
+
"UP", # pyupgrade
|
|
61
|
+
]
|
|
62
|
+
exclude = ["example/rpc"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0-dev.3"
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import functools
|
|
3
|
+
import typing
|
|
4
|
+
import traceback
|
|
5
|
+
|
|
6
|
+
from . import base
|
|
7
|
+
from . import exceptions
|
|
8
|
+
from . import errors
|
|
9
|
+
from . import ctxkeys
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
import contextvars # Python 3.7+ only.
|
|
13
|
+
except ImportError: # pragma: no cover
|
|
14
|
+
contextvars = None # type: ignore
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Lifted from starlette.concurrency
|
|
18
|
+
async def run_in_threadpool(func: typing.Callable, *args: typing.Any, **kwargs: typing.Any) -> typing.Any:
|
|
19
|
+
loop = asyncio.get_event_loop()
|
|
20
|
+
if contextvars is not None: # pragma: no cover
|
|
21
|
+
# Ensure we run in the same context
|
|
22
|
+
child = functools.partial(func, *args, **kwargs)
|
|
23
|
+
context = contextvars.copy_context()
|
|
24
|
+
func = context.run
|
|
25
|
+
args = (child,)
|
|
26
|
+
elif kwargs: # pragma: no cover
|
|
27
|
+
# loop.run_in_executor doesn't accept 'kwargs', so bind them in here
|
|
28
|
+
func = functools.partial(func, **kwargs)
|
|
29
|
+
return await loop.run_in_executor(None, func, *args)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def thread_pool_runner(func):
|
|
33
|
+
async def run(ctx, request):
|
|
34
|
+
return await run_in_threadpool(func, ctx, request)
|
|
35
|
+
|
|
36
|
+
return run
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TwirpASGIApp(base.TwirpBaseApp):
|
|
40
|
+
async def __call__(self, scope, receive, send):
|
|
41
|
+
assert scope["type"] == "http"
|
|
42
|
+
ctx = self._ctx_class()
|
|
43
|
+
try:
|
|
44
|
+
http_method = scope["method"]
|
|
45
|
+
if http_method != "POST":
|
|
46
|
+
raise exceptions.TwirpServerException(
|
|
47
|
+
code=errors.Errors.BadRoute,
|
|
48
|
+
message="unsupported method " + http_method + " (only POST is allowed)",
|
|
49
|
+
meta={"twirp_invalid_route": http_method + " " + scope["path"]},
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
headers = {k.decode("utf-8"): v.decode("utf-8") for (k, v) in scope["headers"]}
|
|
53
|
+
ctx.set(ctxkeys.RAW_REQUEST_PATH, scope["path"])
|
|
54
|
+
ctx.set(ctxkeys.RAW_HEADERS, headers)
|
|
55
|
+
self._hook.request_received(ctx=ctx)
|
|
56
|
+
|
|
57
|
+
endpoint = self._get_endpoint(scope["path"])
|
|
58
|
+
headers = {k.decode("utf-8"): v.decode("utf-8") for (k, v) in scope["headers"]}
|
|
59
|
+
self.validate_content_length(headers=headers)
|
|
60
|
+
encoder, decoder = self._get_encoder_decoder(endpoint, headers)
|
|
61
|
+
|
|
62
|
+
# add headers from request into context
|
|
63
|
+
ctx.set(ctxkeys.SERVICE_NAME, endpoint.service_name)
|
|
64
|
+
ctx.set(ctxkeys.METHOD_NAME, endpoint.name)
|
|
65
|
+
ctx.set(ctxkeys.RESPONSE_STATUS, 200)
|
|
66
|
+
self._hook.request_routed(ctx=ctx)
|
|
67
|
+
raw_receive = await self._recv_all(receive)
|
|
68
|
+
request = decoder(raw_receive)
|
|
69
|
+
response_data = await self._with_middlewares(func=endpoint.function, ctx=ctx, request=request)
|
|
70
|
+
self._hook.response_prepared(ctx=ctx)
|
|
71
|
+
|
|
72
|
+
body_bytes, headers = encoder(response_data)
|
|
73
|
+
headers = dict(ctx.get_response_headers(), **headers)
|
|
74
|
+
# Todo: middleware
|
|
75
|
+
await self._respond(send=send, status=200, headers=headers, body_bytes=body_bytes)
|
|
76
|
+
self._hook.response_sent(ctx=ctx)
|
|
77
|
+
except Exception as e:
|
|
78
|
+
await self.handle_error(ctx, e, scope, receive, send)
|
|
79
|
+
|
|
80
|
+
def _with_middlewares(self, *args, func, ctx, request):
|
|
81
|
+
chain = iter(self._middlewares + (func,))
|
|
82
|
+
|
|
83
|
+
def bind(fn):
|
|
84
|
+
if not asyncio.iscoroutinefunction(fn):
|
|
85
|
+
fn = thread_pool_runner(fn)
|
|
86
|
+
|
|
87
|
+
async def nxt(ctx, request):
|
|
88
|
+
try:
|
|
89
|
+
cur = next(chain)
|
|
90
|
+
return await fn(ctx, request, bind(cur))
|
|
91
|
+
except StopIteration:
|
|
92
|
+
pass
|
|
93
|
+
return await fn(ctx, request)
|
|
94
|
+
|
|
95
|
+
return nxt
|
|
96
|
+
|
|
97
|
+
return bind(next(chain))(ctx, request)
|
|
98
|
+
|
|
99
|
+
async def handle_error(self, ctx, exc, scope, receive, send):
|
|
100
|
+
status = 500
|
|
101
|
+
body_bytes = b"{}"
|
|
102
|
+
logger = ctx.get_logger()
|
|
103
|
+
error_data = {}
|
|
104
|
+
ctx.set(ctxkeys.ORIGINAL_EXCEPTION, exc)
|
|
105
|
+
try:
|
|
106
|
+
if not isinstance(exc, exceptions.TwirpServerException):
|
|
107
|
+
error_data["raw_error"] = str(exc)
|
|
108
|
+
error_data["raw_trace"] = traceback.format_exc()
|
|
109
|
+
logger.exception("got non-twirp exception while processing request", **error_data)
|
|
110
|
+
exc = exceptions.TwirpServerException(code=errors.Errors.Internal, message="Internal non-Twirp Error")
|
|
111
|
+
|
|
112
|
+
body_bytes = exc.to_json_bytes()
|
|
113
|
+
status = errors.Errors.get_status_code(exc.code)
|
|
114
|
+
except Exception:
|
|
115
|
+
exc = exceptions.TwirpServerException(
|
|
116
|
+
code=errors.Errors.Internal, message="There was an error but it could not be serialized into JSON"
|
|
117
|
+
)
|
|
118
|
+
error_data["raw_error"] = str(exc)
|
|
119
|
+
error_data["raw_trace"] = traceback.format_exc()
|
|
120
|
+
logger.exception("got exception while processing request", **error_data)
|
|
121
|
+
body_bytes = exc.to_json_bytes()
|
|
122
|
+
|
|
123
|
+
ctx.set_logger(logger.bind(**error_data))
|
|
124
|
+
ctx.set(ctxkeys.RESPONSE_STATUS, status)
|
|
125
|
+
self._hook.error(ctx=ctx, exc=exc)
|
|
126
|
+
await self._respond(
|
|
127
|
+
send=send, status=status, headers={"Content-Type": "application/json"}, body_bytes=body_bytes
|
|
128
|
+
)
|
|
129
|
+
self._hook.response_sent(ctx=ctx)
|
|
130
|
+
|
|
131
|
+
async def _respond(self, *args, send, status, headers, body_bytes):
|
|
132
|
+
headers["Content-Length"] = str(len(body_bytes))
|
|
133
|
+
resp_headers = [(k.encode("utf-8"), v.encode("utf-8")) for (k, v) in headers.items()]
|
|
134
|
+
await send(
|
|
135
|
+
{
|
|
136
|
+
"type": "http.response.start",
|
|
137
|
+
"status": status,
|
|
138
|
+
"headers": resp_headers,
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
await send(
|
|
142
|
+
{
|
|
143
|
+
"type": "http.response.body",
|
|
144
|
+
"body": body_bytes,
|
|
145
|
+
}
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
async def _recv_all(self, receive):
|
|
149
|
+
body = b""
|
|
150
|
+
more_body = True
|
|
151
|
+
while more_body:
|
|
152
|
+
message = await receive()
|
|
153
|
+
body += message.get("body", b"")
|
|
154
|
+
more_body = message.get("more_body", False)
|
|
155
|
+
|
|
156
|
+
# the body length exceeded than the size set, raise a valid exception
|
|
157
|
+
# so that proper error is returned to the client
|
|
158
|
+
if self._max_receive_message_length < len(body):
|
|
159
|
+
raise exceptions.TwirpServerException(
|
|
160
|
+
code=errors.Errors.InvalidArgument,
|
|
161
|
+
message=f"message body exceeds the specified length of {self._max_receive_message_length} bytes",
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
return body
|
|
165
|
+
|
|
166
|
+
# we will check content-length header value and make sure it is
|
|
167
|
+
# below the limit set
|
|
168
|
+
def validate_content_length(self, headers):
|
|
169
|
+
try:
|
|
170
|
+
content_length = int(headers.get("content-length"))
|
|
171
|
+
except (ValueError, TypeError):
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
if self._max_receive_message_length < content_length:
|
|
175
|
+
raise exceptions.TwirpServerException(
|
|
176
|
+
code=errors.Errors.InvalidArgument,
|
|
177
|
+
message=f"message body exceeds the specified length of {self._max_receive_message_length} bytes",
|
|
178
|
+
)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
import aiohttp
|
|
5
|
+
|
|
6
|
+
from . import exceptions
|
|
7
|
+
from . import errors
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AsyncTwirpClient:
|
|
11
|
+
def __init__(self, address: str, session: aiohttp.ClientSession | None = None) -> None:
|
|
12
|
+
self._address = address
|
|
13
|
+
self._session = session
|
|
14
|
+
|
|
15
|
+
async def _make_request(self, *, url, ctx, request, response_obj, session=None, **kwargs):
|
|
16
|
+
headers = ctx.get_headers()
|
|
17
|
+
if "headers" in kwargs:
|
|
18
|
+
headers.update(kwargs["headers"])
|
|
19
|
+
kwargs["headers"] = headers
|
|
20
|
+
kwargs["headers"]["Content-Type"] = "application/protobuf"
|
|
21
|
+
|
|
22
|
+
if session is None:
|
|
23
|
+
session = self._session
|
|
24
|
+
if not isinstance(session, aiohttp.ClientSession):
|
|
25
|
+
raise TypeError(f"invalid session type '{type(session).__name__}'")
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
async with await session.post(url=url, data=request.SerializeToString(), **kwargs) as resp:
|
|
29
|
+
if resp.status == 200:
|
|
30
|
+
response = response_obj()
|
|
31
|
+
response.ParseFromString(await resp.read())
|
|
32
|
+
return response
|
|
33
|
+
try:
|
|
34
|
+
raise exceptions.TwirpServerException.from_json(await resp.json())
|
|
35
|
+
except (aiohttp.ContentTypeError, json.JSONDecodeError):
|
|
36
|
+
raise exceptions.twirp_error_from_intermediary(
|
|
37
|
+
resp.status, resp.reason, resp.headers, await resp.text()
|
|
38
|
+
) from None
|
|
39
|
+
except asyncio.TimeoutError as e:
|
|
40
|
+
raise exceptions.TwirpServerException(
|
|
41
|
+
code=errors.Errors.DeadlineExceeded,
|
|
42
|
+
message=str(e) or "request timeout",
|
|
43
|
+
meta={"original_exception": e},
|
|
44
|
+
)
|
|
45
|
+
except aiohttp.ServerConnectionError as e:
|
|
46
|
+
raise exceptions.TwirpServerException(
|
|
47
|
+
code=errors.Errors.Unavailable,
|
|
48
|
+
message=str(e),
|
|
49
|
+
meta={"original_exception": e},
|
|
50
|
+
)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
from collections import namedtuple
|
|
3
|
+
|
|
4
|
+
from google.protobuf import json_format
|
|
5
|
+
from google.protobuf import message
|
|
6
|
+
from google.protobuf import symbol_database as _symbol_database
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
from . import context
|
|
10
|
+
|
|
11
|
+
from . import server
|
|
12
|
+
from . import exceptions
|
|
13
|
+
from . import errors
|
|
14
|
+
from . import hook as vtwirp_hook
|
|
15
|
+
|
|
16
|
+
_sym_lookup = _symbol_database.Default().GetSymbol
|
|
17
|
+
|
|
18
|
+
Endpoint = namedtuple("Endpoint", ["service_name", "name", "function", "input", "output"])
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TwirpBaseApp:
|
|
22
|
+
def __init__(self, *middlewares, hook=None, prefix="", max_receive_message_length=1024 * 100 * 100, ctx_class=None):
|
|
23
|
+
self._prefix = prefix
|
|
24
|
+
self._services = {}
|
|
25
|
+
self._max_receive_message_length = max_receive_message_length
|
|
26
|
+
if ctx_class is None:
|
|
27
|
+
ctx_class = context.Context
|
|
28
|
+
assert issubclass(ctx_class, context.Context)
|
|
29
|
+
self._ctx_class = ctx_class
|
|
30
|
+
self._middlewares = middlewares
|
|
31
|
+
if hook is None:
|
|
32
|
+
hook = vtwirp_hook.TwirpHook()
|
|
33
|
+
assert isinstance(hook, vtwirp_hook.TwirpHook)
|
|
34
|
+
self._hook = hook
|
|
35
|
+
|
|
36
|
+
def add_service(self, svc: server.TwirpServer):
|
|
37
|
+
self._services[self._prefix + svc.prefix] = svc
|
|
38
|
+
|
|
39
|
+
def _get_endpoint(self, path):
|
|
40
|
+
svc = self._services.get(path.rsplit("/", 1)[0], None)
|
|
41
|
+
if svc is None:
|
|
42
|
+
raise exceptions.TwirpServerException(code=errors.Errors.NotFound, message="not found")
|
|
43
|
+
|
|
44
|
+
return svc.get_endpoint(path[len(self._prefix) :])
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def json_decoder(body, data_obj=None):
|
|
48
|
+
data = data_obj()
|
|
49
|
+
try:
|
|
50
|
+
json_format.Parse(body, data)
|
|
51
|
+
except json_format.ParseError as exc:
|
|
52
|
+
raise exceptions.TwirpServerException(
|
|
53
|
+
code=errors.Errors.Malformed,
|
|
54
|
+
message="the json request could not be decoded",
|
|
55
|
+
) from exc
|
|
56
|
+
return data
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def json_encoder(value, data_obj=None):
|
|
60
|
+
if not isinstance(value, data_obj):
|
|
61
|
+
raise exceptions.TwirpServerException(
|
|
62
|
+
code=errors.Errors.Internal,
|
|
63
|
+
message=(
|
|
64
|
+
"bad service response type " + str(type(value)) + ", expecting: " + data_obj.DESCRIPTOR.full_name
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return json_format.MessageToJson(value, preserving_proto_field_name=True).encode("utf-8"), {
|
|
69
|
+
"Content-Type": "application/json"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def proto_decoder(body, data_obj=None):
|
|
74
|
+
data = data_obj()
|
|
75
|
+
try:
|
|
76
|
+
data.ParseFromString(body)
|
|
77
|
+
except message.DecodeError as exc:
|
|
78
|
+
raise exceptions.TwirpServerException(
|
|
79
|
+
code=errors.Errors.Malformed,
|
|
80
|
+
message="the protobuf request could not be decoded",
|
|
81
|
+
) from exc
|
|
82
|
+
return data
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def proto_encoder(value, data_obj=None):
|
|
86
|
+
if not isinstance(value, data_obj):
|
|
87
|
+
raise exceptions.TwirpServerException(
|
|
88
|
+
code=errors.Errors.Internal,
|
|
89
|
+
message=(
|
|
90
|
+
"bad service response type " + str(type(value)) + ", expecting: " + data_obj.DESCRIPTOR.full_name
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return value.SerializeToString(), {"Content-Type": "application/protobuf"}
|
|
95
|
+
|
|
96
|
+
def _get_encoder_decoder(self, endpoint, headers):
|
|
97
|
+
ctype = headers.get("content-type", None)
|
|
98
|
+
if "application/json" == ctype:
|
|
99
|
+
decoder = functools.partial(self.json_decoder, data_obj=endpoint.input)
|
|
100
|
+
encoder = functools.partial(self.json_encoder, data_obj=endpoint.output)
|
|
101
|
+
elif "application/protobuf" == ctype:
|
|
102
|
+
decoder = functools.partial(self.proto_decoder, data_obj=endpoint.input)
|
|
103
|
+
encoder = functools.partial(self.proto_encoder, data_obj=endpoint.output)
|
|
104
|
+
else:
|
|
105
|
+
raise exceptions.TwirpServerException(
|
|
106
|
+
code=errors.Errors.BadRoute, message="unexpected Content-Type: " + str(ctype)
|
|
107
|
+
)
|
|
108
|
+
return encoder, decoder
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
|
|
3
|
+
from . import exceptions
|
|
4
|
+
from . import errors
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TwirpClient:
|
|
8
|
+
def __init__(self, address, timeout=5):
|
|
9
|
+
self._address = address
|
|
10
|
+
self._timeout = timeout
|
|
11
|
+
|
|
12
|
+
def _make_request(self, *args, url, ctx, request, response_obj, **kwargs):
|
|
13
|
+
if "timeout" not in kwargs:
|
|
14
|
+
kwargs["timeout"] = self._timeout
|
|
15
|
+
headers = ctx.get_headers()
|
|
16
|
+
if "headers" in kwargs:
|
|
17
|
+
headers.update(kwargs["headers"])
|
|
18
|
+
kwargs["headers"] = headers
|
|
19
|
+
kwargs["headers"]["Content-Type"] = "application/protobuf"
|
|
20
|
+
try:
|
|
21
|
+
resp = requests.post(url=self._address + url, data=request.SerializeToString(), **kwargs)
|
|
22
|
+
if resp.status_code == 200:
|
|
23
|
+
response = response_obj()
|
|
24
|
+
response.ParseFromString(resp.content)
|
|
25
|
+
return response
|
|
26
|
+
try:
|
|
27
|
+
raise exceptions.TwirpServerException.from_json(resp.json())
|
|
28
|
+
except requests.JSONDecodeError:
|
|
29
|
+
raise exceptions.twirp_error_from_intermediary(
|
|
30
|
+
resp.status_code, resp.reason, resp.headers, resp.text
|
|
31
|
+
) from None
|
|
32
|
+
# Todo: handle error
|
|
33
|
+
except requests.exceptions.Timeout as e:
|
|
34
|
+
raise exceptions.TwirpServerException(
|
|
35
|
+
code=errors.Errors.DeadlineExceeded,
|
|
36
|
+
message=str(e),
|
|
37
|
+
meta={"original_exception": e},
|
|
38
|
+
)
|
|
39
|
+
except requests.exceptions.ConnectionError as e:
|
|
40
|
+
raise exceptions.TwirpServerException(
|
|
41
|
+
code=errors.Errors.Unavailable,
|
|
42
|
+
message=str(e),
|
|
43
|
+
meta={"original_exception": e},
|
|
44
|
+
)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from . import logging
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Context:
|
|
5
|
+
"""Context object for storing context information of
|
|
6
|
+
request currently being processed.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
def __init__(self, *args, logger=None, headers=None):
|
|
10
|
+
"""Create a new Context object
|
|
11
|
+
|
|
12
|
+
Keyword arguments:
|
|
13
|
+
logger: Logger that will be used for logging.
|
|
14
|
+
headers: Headers for the request.
|
|
15
|
+
"""
|
|
16
|
+
self._values = {}
|
|
17
|
+
if logger is None:
|
|
18
|
+
logger = logging.get_logger()
|
|
19
|
+
self._logger = logger
|
|
20
|
+
if headers is None:
|
|
21
|
+
headers = {}
|
|
22
|
+
self._headers = headers
|
|
23
|
+
self._response_headers = {}
|
|
24
|
+
|
|
25
|
+
def set(self, key, value):
|
|
26
|
+
"""Set a Context value
|
|
27
|
+
|
|
28
|
+
Arguments:
|
|
29
|
+
key: Key for the context key-value pair.
|
|
30
|
+
value: Value to be stored.
|
|
31
|
+
"""
|
|
32
|
+
self._values[key] = value
|
|
33
|
+
|
|
34
|
+
def get(self, key):
|
|
35
|
+
"""Get a Context value
|
|
36
|
+
|
|
37
|
+
Arguments:
|
|
38
|
+
key: Key for the context key-value pair.
|
|
39
|
+
"""
|
|
40
|
+
return self._values[key]
|
|
41
|
+
|
|
42
|
+
def get_logger(self):
|
|
43
|
+
"""Get current logger used by Context."""
|
|
44
|
+
return self._logger
|
|
45
|
+
|
|
46
|
+
def set_logger(self, logger):
|
|
47
|
+
"""Set logger for this Context
|
|
48
|
+
|
|
49
|
+
Arguments:
|
|
50
|
+
logger: Logger object to be used.
|
|
51
|
+
"""
|
|
52
|
+
self._logger = logger
|
|
53
|
+
|
|
54
|
+
def get_headers(self):
|
|
55
|
+
"""Get request headers that are currently stored."""
|
|
56
|
+
return self._headers
|
|
57
|
+
|
|
58
|
+
def set_header(self, key, value):
|
|
59
|
+
"""Set a request header
|
|
60
|
+
|
|
61
|
+
Arguments:
|
|
62
|
+
key: Key for the header.
|
|
63
|
+
value: Value for the header.
|
|
64
|
+
"""
|
|
65
|
+
self._headers[key] = value
|
|
66
|
+
|
|
67
|
+
def get_response_headers(self):
|
|
68
|
+
"""Get response headers that are currently stored."""
|
|
69
|
+
return self._response_headers
|
|
70
|
+
|
|
71
|
+
def set_response_header(self, key, value):
|
|
72
|
+
"""Set a response header
|
|
73
|
+
|
|
74
|
+
Arguments:
|
|
75
|
+
key: Key for the header.
|
|
76
|
+
value: Value for the header.
|
|
77
|
+
"""
|
|
78
|
+
self._response_headers[key] = value
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Errors(Enum):
|
|
5
|
+
Canceled = "canceled"
|
|
6
|
+
Unknown = "unknown"
|
|
7
|
+
InvalidArgument = "invalid_argument"
|
|
8
|
+
DeadlineExceeded = "deadline_exceeded"
|
|
9
|
+
NotFound = "not_found"
|
|
10
|
+
BadRoute = "bad_route"
|
|
11
|
+
AlreadyExists = "already_exists"
|
|
12
|
+
PermissionDenied = "permission_denied"
|
|
13
|
+
Unauthenticated = "unauthenticated"
|
|
14
|
+
ResourceExhausted = "resource_exhausted"
|
|
15
|
+
FailedPrecondition = "failed_precondition"
|
|
16
|
+
Aborted = "aborted"
|
|
17
|
+
OutOfRange = "out_of_range"
|
|
18
|
+
Unimplemented = "unimplemented"
|
|
19
|
+
Internal = "internal"
|
|
20
|
+
Unavailable = "unavailable"
|
|
21
|
+
DataLoss = "data_loss"
|
|
22
|
+
Malformed = "malformed"
|
|
23
|
+
NoError = ""
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def get_status_code(code):
|
|
27
|
+
return {
|
|
28
|
+
Errors.Canceled: 408,
|
|
29
|
+
Errors.Unknown: 500,
|
|
30
|
+
Errors.InvalidArgument: 400,
|
|
31
|
+
Errors.Malformed: 400,
|
|
32
|
+
Errors.DeadlineExceeded: 408,
|
|
33
|
+
Errors.NotFound: 404,
|
|
34
|
+
Errors.BadRoute: 404,
|
|
35
|
+
Errors.AlreadyExists: 409,
|
|
36
|
+
Errors.PermissionDenied: 403,
|
|
37
|
+
Errors.Unauthenticated: 401,
|
|
38
|
+
Errors.ResourceExhausted: 429,
|
|
39
|
+
Errors.FailedPrecondition: 412,
|
|
40
|
+
Errors.Aborted: 409,
|
|
41
|
+
Errors.OutOfRange: 400,
|
|
42
|
+
Errors.Unimplemented: 501,
|
|
43
|
+
Errors.Internal: 500,
|
|
44
|
+
Errors.Unavailable: 503,
|
|
45
|
+
Errors.DataLoss: 500,
|
|
46
|
+
Errors.NoError: 200,
|
|
47
|
+
}.get(code, 500)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from http.client import HTTPException
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from . import errors
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TwirpServerException(HTTPException):
|
|
9
|
+
def __init__(self, *args, code, message, meta: dict[str, Any] | None = None):
|
|
10
|
+
try:
|
|
11
|
+
self._code = errors.Errors(code)
|
|
12
|
+
except ValueError:
|
|
13
|
+
self._code = errors.Errors.Unknown
|
|
14
|
+
self._message = message
|
|
15
|
+
self._meta = meta or {}
|
|
16
|
+
super().__init__(message)
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def code(self):
|
|
20
|
+
if isinstance(self._code, errors.Errors):
|
|
21
|
+
return self._code
|
|
22
|
+
return errors.Errors.Unknown
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def message(self):
|
|
26
|
+
return self._message
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def meta(self):
|
|
30
|
+
return self._meta
|
|
31
|
+
|
|
32
|
+
def to_dict(self):
|
|
33
|
+
err = {"code": self._code.value, "msg": self._message, "meta": {}}
|
|
34
|
+
for k, v in self._meta.items():
|
|
35
|
+
err["meta"][k] = str(v)
|
|
36
|
+
return err
|
|
37
|
+
|
|
38
|
+
def to_json_bytes(self):
|
|
39
|
+
return json.dumps(self.to_dict()).encode("utf-8")
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def from_json(err_dict):
|
|
43
|
+
return TwirpServerException(
|
|
44
|
+
code=err_dict.get("code", errors.Errors.Unknown),
|
|
45
|
+
message=err_dict.get("msg", ""),
|
|
46
|
+
meta=err_dict.get("meta", {}),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def InvalidArgument(*args, argument, error):
|
|
51
|
+
return TwirpServerException(
|
|
52
|
+
code=errors.Errors.InvalidArgument, message=f"{argument} {error}", meta={"argument": argument}
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def RequiredArgument(*args, argument):
|
|
57
|
+
return InvalidArgument(argument=argument, error="is required")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def twirp_error_from_intermediary(status, reason, headers, body):
|
|
61
|
+
# see https://twitchtv.github.io/twirp/docs/errors.html#http-errors-from-intermediary-proxies
|
|
62
|
+
meta = {
|
|
63
|
+
"http_error_from_intermediary": "true",
|
|
64
|
+
"status_code": str(status),
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if 300 <= status < 400:
|
|
68
|
+
# twirp uses POST which should not redirect
|
|
69
|
+
code = errors.Errors.Internal
|
|
70
|
+
location = headers.get("location")
|
|
71
|
+
message = 'unexpected HTTP status code %d "%s" received, Location="%s"' % (
|
|
72
|
+
status,
|
|
73
|
+
reason,
|
|
74
|
+
location,
|
|
75
|
+
)
|
|
76
|
+
meta["location"] = location
|
|
77
|
+
|
|
78
|
+
else:
|
|
79
|
+
code = {
|
|
80
|
+
400: errors.Errors.Internal, # JSON response should have been returned
|
|
81
|
+
401: errors.Errors.Unauthenticated,
|
|
82
|
+
403: errors.Errors.PermissionDenied,
|
|
83
|
+
404: errors.Errors.BadRoute,
|
|
84
|
+
429: errors.Errors.ResourceExhausted,
|
|
85
|
+
502: errors.Errors.Unavailable,
|
|
86
|
+
503: errors.Errors.Unavailable,
|
|
87
|
+
504: errors.Errors.Unavailable,
|
|
88
|
+
}.get(status, errors.Errors.Unknown)
|
|
89
|
+
|
|
90
|
+
message = 'Error from intermediary with HTTP status code %d "%s"' % (
|
|
91
|
+
status,
|
|
92
|
+
reason,
|
|
93
|
+
)
|
|
94
|
+
meta["body"] = body
|
|
95
|
+
|
|
96
|
+
return TwirpServerException(code=code, message=message, meta=meta)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
class TwirpHook:
|
|
2
|
+
# Called as soon as a request is received, always called
|
|
3
|
+
def request_received(self, *args, ctx):
|
|
4
|
+
pass
|
|
5
|
+
|
|
6
|
+
# Called once the request is routed, service name known, only called if request is routable
|
|
7
|
+
def request_routed(self, *args, ctx):
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
# Called once the response is prepared, not called for error cases
|
|
11
|
+
def response_prepared(self, *args, ctx):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
# Called if an error occurs
|
|
15
|
+
def error(self, *args, ctx, exc):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
# Called after error is sent, always called
|
|
19
|
+
def response_sent(self, *args, ctx):
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ChainHooks(TwirpHook):
|
|
24
|
+
def __init__(self, *hooks):
|
|
25
|
+
for hook in hooks:
|
|
26
|
+
assert isinstance(hook, TwirpHook)
|
|
27
|
+
self._hooks = hooks
|
|
28
|
+
|
|
29
|
+
def request_received(self, *args, ctx):
|
|
30
|
+
for hook in self._hooks:
|
|
31
|
+
hook.request_received(ctx=ctx)
|
|
32
|
+
|
|
33
|
+
def request_routed(self, *args, ctx):
|
|
34
|
+
for hook in self._hooks:
|
|
35
|
+
hook.request_routed(ctx=ctx)
|
|
36
|
+
|
|
37
|
+
def response_prepared(self, *args, ctx):
|
|
38
|
+
for hook in self._hooks:
|
|
39
|
+
hook.response_prepared(ctx=ctx)
|
|
40
|
+
|
|
41
|
+
def error(self, *args, ctx, exc):
|
|
42
|
+
for hook in self._hooks:
|
|
43
|
+
hook.error(ctx=ctx, exc=exc)
|
|
44
|
+
|
|
45
|
+
def response_sent(self, *args, ctx):
|
|
46
|
+
for hook in self._hooks:
|
|
47
|
+
hook.response_sent(ctx=ctx)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
import structlog
|
|
5
|
+
from structlog.stdlib import LoggerFactory, add_log_level
|
|
6
|
+
|
|
7
|
+
_configured = False
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def configure(force=False):
|
|
11
|
+
"""
|
|
12
|
+
Configures logging & structlog modules
|
|
13
|
+
|
|
14
|
+
Keyword Arguments:
|
|
15
|
+
force: Force to reconfigure logging.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
global _configured
|
|
19
|
+
if _configured and not force:
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
# Check whether debug flag is set
|
|
23
|
+
debug = os.environ.get("DEBUG_MODE", False)
|
|
24
|
+
# Set appropriate log level
|
|
25
|
+
if debug:
|
|
26
|
+
log_level = logging.DEBUG
|
|
27
|
+
else:
|
|
28
|
+
log_level = logging.INFO
|
|
29
|
+
|
|
30
|
+
# Set logging config
|
|
31
|
+
logging.basicConfig(
|
|
32
|
+
level=log_level,
|
|
33
|
+
format="%(message)s",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Configure structlog
|
|
37
|
+
structlog.configure(
|
|
38
|
+
logger_factory=LoggerFactory(),
|
|
39
|
+
processors=[
|
|
40
|
+
add_log_level,
|
|
41
|
+
# Add timestamp
|
|
42
|
+
structlog.processors.TimeStamper("iso"),
|
|
43
|
+
# Add stack information
|
|
44
|
+
structlog.processors.StackInfoRenderer(),
|
|
45
|
+
# Set exception field using exec info
|
|
46
|
+
structlog.processors.format_exc_info,
|
|
47
|
+
# Render event_dict as JSON
|
|
48
|
+
structlog.processors.JSONRenderer(),
|
|
49
|
+
],
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
_configured = True
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_logger(**kwargs):
|
|
56
|
+
"""
|
|
57
|
+
Get the structlog logger
|
|
58
|
+
"""
|
|
59
|
+
# Configure logging modules
|
|
60
|
+
configure()
|
|
61
|
+
# Return structlog
|
|
62
|
+
return structlog.get_logger(**kwargs)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from . import exceptions
|
|
2
|
+
from . import errors
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TwirpServer:
|
|
6
|
+
def __init__(self, *args, service):
|
|
7
|
+
self.service = service
|
|
8
|
+
self._endpoints = {}
|
|
9
|
+
self._prefix = ""
|
|
10
|
+
|
|
11
|
+
@property
|
|
12
|
+
def prefix(self):
|
|
13
|
+
return self._prefix
|
|
14
|
+
|
|
15
|
+
def get_endpoint(self, path):
|
|
16
|
+
(_, url_pre, rpc_method) = path.rpartition(self._prefix + "/")
|
|
17
|
+
if not url_pre or not rpc_method:
|
|
18
|
+
raise exceptions.TwirpServerException(
|
|
19
|
+
code=errors.Errors.BadRoute,
|
|
20
|
+
message="no handler for path " + path,
|
|
21
|
+
meta={"twirp_invalid_route": "POST " + path},
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
endpoint = self._endpoints.get(rpc_method, None)
|
|
25
|
+
if not endpoint:
|
|
26
|
+
raise exceptions.TwirpServerException(
|
|
27
|
+
code=errors.Errors.Unimplemented,
|
|
28
|
+
message="service has no endpoint " + rpc_method,
|
|
29
|
+
meta={"twirp_invalide_route": "POST " + path},
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
return endpoint
|