Update
This commit is contained in:
33
docs/source/backend/flask.py
Executable file
33
docs/source/backend/flask.py
Executable file
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from jsonrpc import JSONRPC
|
||||
from jsonrpc.backend.flask import APIView
|
||||
from flask import Flask
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
jsonrpc = JSONRPC()
|
||||
app.add_url_rule('/api', view_func=APIView.as_view('api', jsonrpc=jsonrpc))
|
||||
|
||||
|
||||
@jsonrpc.method('boo')
|
||||
def index() -> str:
|
||||
return 'Welcome to JSON-RPC'
|
||||
|
||||
|
||||
def link_page_tag(tag: int, page: int) -> str:
|
||||
return f'tag: {tag}\npage: {page}'
|
||||
|
||||
|
||||
jsonrpc['tag.page'] = link_page_tag
|
||||
jsonrpc['page.tag'] = link_page_tag
|
||||
|
||||
|
||||
def raise_error() -> bool:
|
||||
raise ValueError("raise ValueError")
|
||||
return True
|
||||
|
||||
|
||||
jsonrpc['raise.error'] = raise_error
|
||||
|
||||
app.run(host='0.0.0.0', debug=True)
|
||||
4
docs/source/backend/flask.rst
Normal file
4
docs/source/backend/flask.rst
Normal file
@@ -0,0 +1,4 @@
|
||||
Flask
|
||||
=====
|
||||
|
||||
.. literalinclude:: flask.py
|
||||
8
docs/source/backend/index.rst
Normal file
8
docs/source/backend/index.rst
Normal file
@@ -0,0 +1,8 @@
|
||||
Сервисы
|
||||
=======
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Содержание:
|
||||
|
||||
flask
|
||||
19
docs/source/usage.py
Normal file
19
docs/source/usage.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from jsonrpc import JSONRPC
|
||||
|
||||
|
||||
jsonrpc = JSONRPC()
|
||||
|
||||
@jsonrpc.method('app.endpoint')
|
||||
def app_endpoint(a: int, b: int) -> int:
|
||||
result = a + b
|
||||
return result
|
||||
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "app.endpoint",
|
||||
"params": {"a": 1, "b": 2},
|
||||
"id": "1"
|
||||
}
|
||||
response = jsonrpc(request)
|
||||
|
||||
print(response)
|
||||
13
pyproject.toml
Normal file
13
pyproject.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[project]
|
||||
name = "jsonrpc"
|
||||
version = "0.3.2"
|
||||
authors = [
|
||||
{ name="RemiZOffAlex", email="remizoffalex@gmail.com" },
|
||||
]
|
||||
maintainers = [
|
||||
{ name="RemiZOffAlex", email="remizoffalex@gmail.com" },
|
||||
]
|
||||
description = ""
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
keywords = ["api", "json", "json-rpc", "rpc"]
|
||||
13
setup.py
13
setup.py
@@ -1,13 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from setuptools import setup, find_packages
|
||||
from setuptools import setup
|
||||
|
||||
|
||||
setup(
|
||||
name='jsonrpc',
|
||||
version='0.3.0',
|
||||
author='RemiZOffAlex',
|
||||
author_email='remizoffalex@gmail.com',
|
||||
packages=find_packages(exclude=['prototypes', 'tests']),
|
||||
keywords=['api', 'json', 'json-rpc', 'rpc']
|
||||
)
|
||||
if __name__ == "__main__":
|
||||
setup()
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
__author__ = 'RemiZOffAlex'
|
||||
__email__ = 'remizoffalex@mail.ru'
|
||||
|
||||
import json
|
||||
import logging
|
||||
import traceback
|
||||
@@ -24,18 +27,21 @@ class Response:
|
||||
|
||||
|
||||
class Method:
|
||||
def __init__(self, function, pre = None):
|
||||
def __init__(self, function, pre = None, debug: bool = False):
|
||||
self.function = function
|
||||
self.pre = pre
|
||||
self.debug = debug
|
||||
|
||||
def __call__(self, query):
|
||||
params = None
|
||||
if 'params' in query:
|
||||
params = query['params']
|
||||
if self.debug:
|
||||
log.error(params)
|
||||
if isinstance(self.pre, list):
|
||||
pass
|
||||
elif type(self.pre).__name__=='function':
|
||||
for item in self.pre:
|
||||
item(query)
|
||||
elif callable(self.pre):
|
||||
self.pre(query)
|
||||
|
||||
|
||||
@@ -66,8 +72,9 @@ class Wrapper:
|
||||
class JSONRPC:
|
||||
"""Основной класс JSON-RPC
|
||||
"""
|
||||
def __init__(self):
|
||||
def __init__(self, debug: bool = False):
|
||||
self.methods = {}
|
||||
self.debug = debug
|
||||
|
||||
def method(self, name: str, pre = None):
|
||||
"""Декоратор метода
|
||||
@@ -93,7 +100,8 @@ class JSONRPC:
|
||||
# for key in sig.parameters:
|
||||
# print(sig.parameters[key].annotation)
|
||||
result = {
|
||||
'name': getattr(method.function, '__name__', None),
|
||||
'name': name,
|
||||
'function': getattr(method.function, '__name__', None),
|
||||
'summary': getattr(method.function, '__doc__', None),
|
||||
'params': [
|
||||
{'name': k, 'type': sig.parameters[k].annotation.__name__}
|
||||
17
src/jsonrpc/backend/README.md
Normal file
17
src/jsonrpc/backend/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Backends
|
||||
|
||||
## aiohttp
|
||||
|
||||
```
|
||||
from jsonrpc.backend.aiohttp import APIView
|
||||
|
||||
app.router.add_view('/api', APIView)
|
||||
```
|
||||
|
||||
## Flask
|
||||
|
||||
```
|
||||
from jsonrpc.backend.flask import APIView
|
||||
|
||||
app.add_url_rule('/api', view_func=APIView.as_view('api', jsonrpc=jsonrpc))
|
||||
```
|
||||
44
src/jsonrpc/backend/aiohttp.py
Normal file
44
src/jsonrpc/backend/aiohttp.py
Normal file
@@ -0,0 +1,44 @@
|
||||
__author__ = 'RemiZOffAlex'
|
||||
__email__ = 'remizoffalex@mail.ru'
|
||||
|
||||
import jinja2
|
||||
import pathlib
|
||||
import aiohttp_jinja2
|
||||
|
||||
from aiohttp.web import View, Response
|
||||
|
||||
from .. import JSONRPC
|
||||
|
||||
|
||||
pathlib.Path(__file__).parent.resolve()
|
||||
|
||||
class APIHandler:
|
||||
def __init__(self, jsonrpc):
|
||||
print(self)
|
||||
self.jsonrpc = jsonrpc
|
||||
|
||||
@aiohttp_jinja2.template('api_browse.html')
|
||||
async def get(self, request) -> Response:
|
||||
pagedata = {
|
||||
'title': 'API Browse',
|
||||
'request': request
|
||||
}
|
||||
return pagedata
|
||||
|
||||
async def post(self, request) -> Response:
|
||||
json_data = await request.json()
|
||||
result = jsonrpc(json_data)
|
||||
return Response(result=result)
|
||||
|
||||
|
||||
def api_init(app, jsonrpc: JSONRPC, rule: str = '/api'):
|
||||
aiohttp_jinja2.setup(
|
||||
app,
|
||||
enable_async=True,
|
||||
loader=jinja2.FileSystemLoader(
|
||||
pathlib.Path(__file__).parent.resolve() / 'templates'
|
||||
)
|
||||
)
|
||||
handler = APIHandler(jsonrpc)
|
||||
app.router.add_route('GET', rule, handler.get)
|
||||
app.router.add_route('POST', rule, handler.post)
|
||||
@@ -11,8 +11,12 @@ from ..exceptions import ParseError
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def to_json(request_data: bytes) -> Any:
|
||||
log.info(request_data)
|
||||
def to_json(
|
||||
request_data: bytes,
|
||||
debug: bool = False
|
||||
) -> Any:
|
||||
if debug:
|
||||
log.debug(request_data)
|
||||
try:
|
||||
return json.loads(request_data)
|
||||
except ValueError as e:
|
||||
@@ -23,9 +27,15 @@ def to_json(request_data: bytes) -> Any:
|
||||
|
||||
|
||||
class APIView(MethodView):
|
||||
def __init__(self, jsonrpc):
|
||||
def __init__(
|
||||
self,
|
||||
jsonrpc,
|
||||
debug: bool = False
|
||||
):
|
||||
self.jsonrpc = jsonrpc
|
||||
log.debug('Connect JSON-RPC to Flask complete')
|
||||
self.debug = debug
|
||||
if debug:
|
||||
log.debug('Connect JSON-RPC to Flask complete')
|
||||
|
||||
def get(self):
|
||||
pagedata = {'title': 'API Browse'}
|
||||
@@ -35,5 +45,6 @@ class APIView(MethodView):
|
||||
def post(self):
|
||||
json_data = to_json(request.data)
|
||||
result = self.jsonrpc(json_data)
|
||||
log.error(result)
|
||||
if debug:
|
||||
log.debug(result)
|
||||
return jsonify(result)
|
||||
18
src/jsonrpc/backend/templates/api_browse.html
Normal file
18
src/jsonrpc/backend/templates/api_browse.html
Normal file
@@ -0,0 +1,18 @@
|
||||
{% extends "skeleton.html" %}
|
||||
{% block content %}
|
||||
|
||||
<h3>API (JSON-RPC 2.0)</h3>
|
||||
<hr />
|
||||
|
||||
<p>Браузер для API (JSON-RPC 2.0) поможет просмотреть список поддерживаемых методов, позволит отправить запросы, получить данные и отобразить результаты.</p>
|
||||
|
||||
{% include '/inc/js-stub.html' %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
<ul>
|
||||
<li><a href="/"><i class="fa fa-home"></i></a></li>
|
||||
<li>API</li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
202
src/jsonrpc/backend/templates/api_browse.js
Normal file
202
src/jsonrpc/backend/templates/api_browse.js
Normal file
@@ -0,0 +1,202 @@
|
||||
function APIBrowse() {
|
||||
let data = {
|
||||
filter: PanelFilter(),
|
||||
raw_methods: [],
|
||||
get methods() {
|
||||
let result = data.raw_methods.filter(method_filter);
|
||||
return result;
|
||||
},
|
||||
current: null,
|
||||
result: null,
|
||||
values: {},
|
||||
};
|
||||
function params() {
|
||||
let result = {};
|
||||
for (let index = 0; index < vm.methods[vm.current].params.length; index++) {
|
||||
const element = vm.methods[vm.current].params[index];
|
||||
result[element.name] = vm.values[element.name];
|
||||
}
|
||||
return result;
|
||||
};
|
||||
function method_filter(method) {
|
||||
/* Отфильтрованный список */
|
||||
let filter = data.filter.data;
|
||||
let value = filter.value;
|
||||
if ( value.length<1 ) {
|
||||
return true;
|
||||
}
|
||||
if ( method.name.toLowerCase().includes(value.toLowerCase()) ) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
function breadcrumbs_render() {
|
||||
let result = m('ul', {class: 'breadcrumb mt-3'}, [
|
||||
m('li', {class: 'breadcrumb-item'}, m(m.route.Link, {href: '/'}, m('i', {class: 'fa fa-home'}))),
|
||||
m('li', {class: 'breadcrumb-item active'}, 'API (JSON-RPC 2.0)'),
|
||||
]);
|
||||
return result;
|
||||
};
|
||||
function method_select(method) {
|
||||
data.current = method;
|
||||
data.values = {};
|
||||
data.result = null;
|
||||
for (let index = 0; index < data.current.params.length; index++) {
|
||||
const element = data.current.params[index];
|
||||
if (element.type==='int') {
|
||||
data.values[element.name] = 0;
|
||||
}
|
||||
if (element.type==='str') {
|
||||
data.values[element.name] = "";
|
||||
}
|
||||
}
|
||||
};
|
||||
function send() {
|
||||
// Добавить нового пользователя
|
||||
let params = {};
|
||||
for (let index = 0; index < data.current.params.length; index++) {
|
||||
const element = data.current.params[index];
|
||||
params[element.name] = data.values[element.name];
|
||||
}
|
||||
console.log(params);
|
||||
m.request({
|
||||
url: '/api',
|
||||
method: "POST",
|
||||
body: {
|
||||
"jsonrpc": "2.0",
|
||||
"method": data.current.name,
|
||||
"params": params,
|
||||
"id": 1
|
||||
}
|
||||
}).then(
|
||||
function(response) {
|
||||
data.result = response;
|
||||
}
|
||||
);
|
||||
};
|
||||
function methods_get() {
|
||||
m.request({
|
||||
url: '/api',
|
||||
method: "POST",
|
||||
body: {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "api.methods",
|
||||
"id": 1
|
||||
}
|
||||
}).then(
|
||||
function(response) {
|
||||
if ('result' in response) {
|
||||
data.raw_methods = response['result'];
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
function method_renders(method, methodIdx) {
|
||||
let odd = '';
|
||||
if (methodIdx % 2) {
|
||||
odd = ' bg-light'
|
||||
};
|
||||
return m('div', {class: 'row'},
|
||||
m('div', {class: `col py-2 ${odd}`, onclick: function() {method_select(method)}}, method.name)
|
||||
// m('div', {class: `col py-2 ${odd}`, onclick: function() {data.current = method}}, method.name)
|
||||
);
|
||||
};
|
||||
function methods_render() {
|
||||
return data.methods.map(method_renders)
|
||||
};
|
||||
function param_value_render(param) {
|
||||
if (param.type==='int') {
|
||||
return m('div', {class: 'input-group'},
|
||||
m('input', {class: 'form-control', type: 'number', oninput: function (e) {data.values[param.name] = e.target.value}, value: data.values[param.name]})
|
||||
)
|
||||
} else {
|
||||
return m('textarea', {class: 'form-control', oninput: function (e) {data.values[param.name] = e.target.value}, value: data.values[param.name]});
|
||||
}
|
||||
};
|
||||
function param_render(param, paramIdx) {
|
||||
let odd = '';
|
||||
if (paramIdx % 2) {
|
||||
odd = ' bg-light'
|
||||
};
|
||||
return m('div', {class: 'row'},
|
||||
// m('div', {class: `col py-2 ${odd}`, onclick: function() {method_select(method)}}, method.name)
|
||||
m('div', {class: `col py-2 ${odd}`}, [
|
||||
m('label', `${param.name}: ${param.type}`),
|
||||
param_value_render(param),
|
||||
])
|
||||
);
|
||||
};
|
||||
function params_render() {
|
||||
return data.current.params.map(param_render)
|
||||
};
|
||||
function current_render() {
|
||||
if (data.current) {
|
||||
return m('div', {class: 'col-md-8'}, [
|
||||
m('h2', data.current.name),
|
||||
m('h3', 'Описание'),
|
||||
m('p', data.current.summary),
|
||||
m('h3', 'Параметры'),
|
||||
params_render(),
|
||||
m('h3', 'Возвращаемое значение'),
|
||||
m('p', `Тип: ${ data.current.return }`),
|
||||
m('h3', 'Пример вызова'),
|
||||
m('div', {class: 'card'},
|
||||
m('div', {class: 'card-body'},
|
||||
m('pre', `$ curl -i -X POST \
|
||||
-H "Content-Type: application/json; indent=4" \
|
||||
-d '{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "${ data.current.name }",
|
||||
"params": { params },
|
||||
"id": "1"
|
||||
}' { request.url_root }api`
|
||||
),
|
||||
),
|
||||
),
|
||||
m('div', {class: 'row'},
|
||||
m('div', {class: 'col py-2'},
|
||||
m('button', {class: 'btn btn-outline-success btn-lg float-end', type: 'submit', onclick: send}, 'Вызвать'),
|
||||
),
|
||||
),
|
||||
m('div', {class: 'card'},
|
||||
m('div', {class: 'card-body'},
|
||||
m('pre', JSON.stringify(data.result, null, 4 )),
|
||||
),
|
||||
),
|
||||
])
|
||||
}
|
||||
};
|
||||
return {
|
||||
oninit: function(vnode) {
|
||||
console.log('APIBrowse.oninit');
|
||||
methods_get();
|
||||
},
|
||||
view: function(vnode) {
|
||||
console.log('APIBrowse.view');
|
||||
result = [
|
||||
breadcrumbs_render(),
|
||||
m('div', {class: 'row'},
|
||||
m('div', {class: 'col h1 py-2'}, [
|
||||
m('button', {type: "button", class: "btn btn-outline-secondary btn-lg me-2", onclick: function() { panel_show(data.filter.data) }},
|
||||
m('i', {class: "fa fa-filter"})
|
||||
),
|
||||
'API (JSON-RPC 2.0)',
|
||||
])
|
||||
),
|
||||
m('hr'),
|
||||
m('p', 'Браузер для API (JSON-RPC 2.0) поможет просмотреть список поддерживаемых методов, позволит отправить запросы, получить данные и отобразить результаты.'),
|
||||
m(data.filter),
|
||||
m('div', {class: 'row', style: 'min-height: 300px;'},
|
||||
m('div', {class: 'col-md-4 overflow-auto position-relative'},
|
||||
m('div', {class: 'position-absolute w-100'},
|
||||
methods_render()
|
||||
),
|
||||
),
|
||||
current_render(),
|
||||
),
|
||||
breadcrumbs_render(),
|
||||
];
|
||||
return result;
|
||||
}
|
||||
};
|
||||
};
|
||||
2
src/jsonrpc/backend/templates/skeleton.html
Normal file
2
src/jsonrpc/backend/templates/skeleton.html
Normal file
@@ -0,0 +1,2 @@
|
||||
{% block content %}
|
||||
{% endblock content %}
|
||||
41
src/jsonrpc/client.py
Normal file
41
src/jsonrpc/client.py
Normal file
@@ -0,0 +1,41 @@
|
||||
__author__ = 'RemiZOffAlex'
|
||||
__email__ = 'remizoffalex@mail.ru'
|
||||
|
||||
import json
|
||||
import logging
|
||||
import requests
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Client:
|
||||
def __init__(
|
||||
self,
|
||||
url: str = 'http://127.0.0.1:5000/api',
|
||||
debug: bool = False
|
||||
):
|
||||
self.url = url
|
||||
self.headers = {'content-type': 'application/json'}
|
||||
self.debug = debug
|
||||
|
||||
def __call__(self, queries):
|
||||
"""Вызов метода
|
||||
"""
|
||||
if isinstance(queries, str):
|
||||
payload = queries
|
||||
elif isinstance(queries, dict | list):
|
||||
payload = json.dumps(queries)
|
||||
response = requests.post(
|
||||
self.url,
|
||||
data=payload,
|
||||
headers=self.headers
|
||||
).json()
|
||||
|
||||
assert 'jsonrpc' in response
|
||||
assert 'id' in response
|
||||
assert response["jsonrpc"] in ['2.0', '3.0']
|
||||
if '3.0' in response["jsonrpc"]:
|
||||
assert 'meta' in response
|
||||
|
||||
return response
|
||||
@@ -1,3 +1,7 @@
|
||||
__author__ = 'RemiZOffAlex'
|
||||
__email__ = 'remizoffalex@mail.ru'
|
||||
|
||||
|
||||
class JSONRPCError(Exception):
|
||||
def __init__(self, id: int, message):
|
||||
pass
|
||||
12
src/jsonrpc/state.py
Normal file
12
src/jsonrpc/state.py
Normal file
@@ -0,0 +1,12 @@
|
||||
__author__ = 'RemiZOffAlex'
|
||||
__email__ = 'remizoffalex@mail.ru'
|
||||
|
||||
|
||||
class StateManager:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
|
||||
class State:
|
||||
def __init__(self):
|
||||
pass
|
||||
Reference in New Issue
Block a user