This commit is contained in:
2024-03-18 03:41:54 +03:00
parent 7aaa8f7573
commit ab33ba3763
18 changed files with 449 additions and 23 deletions

33
docs/source/backend/flask.py Executable file
View 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)

View File

@@ -0,0 +1,4 @@
Flask
=====
.. literalinclude:: flask.py

View File

@@ -0,0 +1,8 @@
Сервисы
=======
.. toctree::
:maxdepth: 2
:caption: Содержание:
flask

19
docs/source/usage.py Normal file
View 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
View 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"]

View File

@@ -1,3 +0,0 @@
[metadata]
name = jsonrpc
version = 0.3.0

View File

@@ -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()

View File

@@ -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__}

View 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))
```

View 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)

View File

@@ -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)

View 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 %}

View 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;
}
};
};

View File

@@ -0,0 +1,2 @@
{% block content %}
{% endblock content %}

41
src/jsonrpc/client.py Normal file
View 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

View File

@@ -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
View File

@@ -0,0 +1,12 @@
__author__ = 'RemiZOffAlex'
__email__ = 'remizoffalex@mail.ru'
class StateManager:
def __init__(self):
pass
class State:
def __init__(self):
pass