Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
abersheeran committed Aug 11, 2019
1 parent 03bbbae commit 24f0af0
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 35 deletions.
15 changes: 15 additions & 0 deletions .gitignore
Expand Up @@ -93,3 +93,18 @@ ENV/
.idea/

.vscode/

# example
Werkzeug-0.15.5.dist-info
werkzeug
MarkupSafe-1.1.1.dist-info
markupsafe
Jinja2-2.10.1.dist-info
jinja2
itsdangerous-1.1.0.dist-info
itsdangerous
Flask-1.1.1.dist-info
flask
Click-7.0.dist-info
click
bin
47 changes: 47 additions & 0 deletions README.md
@@ -1 +1,48 @@
# aligi

为无服务函数计算作为阿里云 API 后端运行提供转析。

## 为什么会有这个库?

使用 Flask,Django 等支持标准 WSGI 协议的 Web 框架创建无服务函数时,使用HTTP触发器才能让它们正常运行。

但如果想使用无服务函数作为阿里云 API 网关的后端时,则无法直接使用这些函数,只能通过网络调用,这显然是不够高效、并且费钱的。

## 如何安装

在项目根目录下执行

```
pip install -t . aligi
```

## 如何使用

以下为一个最小的 Flask 样例

```python
import json

from flask import Flask
from aligi.wsgi import WSGI
from aligi.types import FCContext

app = Flask(__name__)


@app.route('/')
def hello_world():
return 'Hello, World!'


def handler(event: str, context: FCContext):
"""阿里云无服务函数的入口"""
wsgi = WSGI(event, context)
wsgi.mount(app)
return wsgi.get_response()
```

其中`app`可以是任意一个标准 WSGI Application。

在 Django 项目中,它一般在项目的`wsgi.py`里。

2 changes: 1 addition & 1 deletion aligi/__version__.py
@@ -1,3 +1,3 @@
VERSION = (0, 0, 0)
VERSION = (0, 0, 1)

__version__ = '.'.join(map(str, VERSION))
13 changes: 7 additions & 6 deletions aligi/core.py
Expand Up @@ -4,12 +4,17 @@
from aligi import types


class Request:
class HTTPRequest:
"""解析阿里云API网关传递的数据"""

def __init__(self, event: str, context: types.FCContext):
self.context = context
self.event = json.loads(event)
self.header = self.event['headers']
self._header = {
"CONTENT-TYPE": "text/plain",
"CONTENT-LENGTH": 0,
}
self._header.update({k.upper(): v for k, v in self.event['headers'].items()})

@property
def path(self) -> str:
Expand All @@ -23,10 +28,6 @@ def method(self) -> str:
def header(self) -> dict:
return self._header

@header.setter
def set_header(self, value: dict) -> None:
self._header = {k.upper(): v for k, v in value.items()}

@property
def query(self) -> dict:
return self.event['queryParameters']
Expand Down
12 changes: 11 additions & 1 deletion aligi/types.py
Expand Up @@ -81,4 +81,14 @@ def __init__(
self.account_id = account_id


WSGIApp = typing.Callable[typing.Dict, typing.Callable(str, typing.Iterable(typing.Tuple(str, str)))]
WSGIApp = typing.Callable[
[
typing.Dict,
typing.Callable[
[
str,
typing.Iterable[typing.Tuple[str, str]]
], None
]
], typing.Iterable
]
87 changes: 60 additions & 27 deletions aligi/wsgi.py
@@ -1,24 +1,54 @@
import io
import json
import typing
import base64

from aligi.types import FCContext, WSGIApp
from aligi.core import Request
from aligi.core import HTTPRequest


class BodyTypeError(Exception):
pass


class ErrorWriter:
"""处理错误日志则继承于此"""

def flush(self) -> None:
pass

def write(self, msg: str) -> None:
pass

def writelines(self, seq: str) -> None:
pass


class WSGI:

def __init__(self, event: str, context: FCContext):
self.request = Request(event, context)
self.set_environ()
self.data = {}

def set_environ(self):
self.request = HTTPRequest(event, context)
self.environ = {
"wsgi.version": (1, 0),
"wsgi.url_scheme": "http",
"wsgi.input": io.BytesIO(self.request.body),
"wsgi.errors": self.errors,
"wsgi.multithread": False,
"wsgi.multiprocess": False,
"wsgi.run_once": True,
}
self.update_environ()
self.raw_data = {}

@property
def errors(self) -> ErrorWriter:
"""
如果需要自定义处理错误日志, 则需要覆盖此属性
"""
return ErrorWriter()

def update_environ(self) -> None:
self.environ.update({
"REQUEST_METHOD": self.request.method,
"SCRIPT_NAME": "",
"PATH_INFO": self.request.path,
Expand All @@ -28,31 +58,34 @@ def set_environ(self):
"SERVER_NAME": "127.0.0.1",
"SERVER_PORT": "80",
"SERVER_PROTOCOL": "HTTP/1.1",
}
self.environ.update({f"HTTP_{k}": v for k, v in self.request.header})
})
self.environ.update({f"HTTP_{k}": v for k, v in self.request.header.items()})

def start_response(self, status: str, headers: typing.Iterable(typing.Tuple(str, str)), exc_info: str = ""):
self.data.update({
def start_response(
self,
status: str,
headers: typing.Iterable[typing.Tuple[str, str]],
exc_info: str = ""
) -> None:
"""
WSGI 标准 start_response, 传递状态码与HTTP头部
"""
self.raw_data.update({
"statusCode": int(status[:3]),
"headers": {
header[0]: header[1] for header in headers
},
})

def mount(self, wsgi: WSGIApp):
body = wsgi(self.environ, self.start_response)
if isinstance(body, bytes):
self.data.update({
"body": base64.b64encode(b"".join(body)),
"isBase64Encoded": True
})
elif isinstance(body, str):
self.data.update({
"body": body,
"isBase64Encoded": False
})
else:
raise BodyTypeError(f"body type: {type(body)}")

def get_response(self):
return json.dumps(self.data)
def mount(self, wsgi: WSGIApp) -> None:
"""挂载并调用标准 WSGI Application"""
body: typing.Iterable = wsgi(self.environ, self.start_response)
body = b"".join(body)
self.raw_data.update({
"body": base64.b64encode(body).decode("utf8"),
"isBase64Encoded": True
})

def get_response(self) -> str:
"""返回 JSON 字符串的结果"""
return json.dumps(self.raw_data)
34 changes: 34 additions & 0 deletions example.py
@@ -0,0 +1,34 @@
import json

from flask import Flask
from aligi.wsgi import WSGI
from aligi.types import FCContext

app = Flask(__name__)


@app.route('/')
def hello_world():
return 'Hello, World!'


def handler(event: str, context: FCContext):
"""阿里云无服务函数的入口"""
wsgi = WSGI(event, context)
wsgi.mount(app)
return wsgi.get_response()


if __name__ == "__main__":
print(handler(json.dumps({
"path": "/",
"httpMethod": "GET",
"headers": {
"Content-Length": 0,
"Content-Type": "text/plain"
},
"queryParameters": {},
"pathParameters": {},
"body": "",
"isBase64Encoded": False
}), None))

0 comments on commit 24f0af0

Please sign in to comment.