Commit d6de5523 authored by inso's avatar inso

Merge branch 'dev'

parents 01b904de 1cc10e2f
......@@ -16,9 +16,9 @@
# Caner Candan <caner@candan.fr>, http://caner.candan.fr
#
PROTOCOL_VERSION="1"
PROTOCOL_VERSION="6"
MANAGED_API=["BASIC_MERKLED_API"]
MANAGED_API=["BASIC_MERKLED_API", "BMAS"]
__author__ = 'Caner Candan & inso'
__version__ = '0.30.8'
......
......@@ -26,5 +26,5 @@ import logging
logger = logging.getLogger("duniter")
from .api import API, ConnectionHandler
from .api import API, ConnectionHandler, parse_error, parse_response, parse_text
from . import network, blockchain, tx, wot, node, ud, ws
\ No newline at end of file
......@@ -18,65 +18,121 @@
# Inso <insomniak.fr at gmail.com>
import aiohttp, json, logging, jsonschema
import aiohttp
import json
import logging
import jsonschema
from ..errors import DuniterError
logger = logging.getLogger("duniter")
ERROR_SCHEMA = {
"type": "object",
"properties": {
"ucode": {
"type": "number"
},
"message": {
"type": "string"
}
},
"required": ["ucode", "message"]
}
class ConnectionHandler(object):
"""Helper class used by other API classes to ease passing server connection information."""
def __init__(self, server, port):
"""
Arguments:
- `server`: server hostname
- `port`: port number
def __init__(self, http_scheme, ws_scheme, server, port, proxy=None, session=None):
"""
Init instance of connection handler
:param str server: Server IP or domaine name
:param int port: Port
:param aiohttp.ClientSession|None session: Session AIOHTTP
"""
self.http_scheme = http_scheme
self.ws_scheme = ws_scheme
self.server = server
self.proxy = proxy
self.port = port
self.session = session
def __str__(self):
return 'connection info: %s:%d' % (self.server, self.port)
def parse_text(text, schema):
"""
Validate and parse the BMA answer from websocket
:param str text: the bma answer
:return: the json data
"""
try:
data = json.loads(text)
jsonschema.validate(data, schema)
return data
except (TypeError, json.decoder.JSONDecodeError):
raise jsonschema.ValidationError("Could not parse json")
def parse_error(text):
"""
Validate and parse the BMA answer from websocket
:param str text: the bma error
:return: the json data
"""
try:
data = json.loads(text)
jsonschema.validate(data, ERROR_SCHEMA)
return data
except (TypeError, json.decoder.JSONDecodeError):
raise jsonschema.ValidationError("Could not parse json")
async def parse_response(response, schema):
"""
Validate and parse the BMA answer
:param aiohttp.ClientResponse response: Response of aiohttp request
:param dict schema: The expected response structure
:return: the json data
"""
try:
data = await response.json()
response.close()
if schema is not None:
jsonschema.validate(data, schema)
return data
except (TypeError, json.decoder.JSONDecodeError) as e:
raise jsonschema.ValidationError("Could not parse json : {0}".format(str(e)))
class API(object):
"""APIRequest is a class used as an interface. The intermediate derivated classes are the modules and the leaf classes are the API requests."""
error_schema = {
"type": "object",
"properties": {
"ucode": {
"type": "number"
},
"message": {
"type": "string"
}
},
"required": ["ucode", "message"]
}
schema = {}
def __init__(self, connection_handler, module):
"""
Asks a module in order to create the url used then by derivated classes.
Arguments:
- `module`: module name
- `connection_handler`: connection handler
:param ConnectionHandler connection_handler: Connection handler
:param str module: Module path
"""
self.module = module
self.connection_handler = connection_handler
self.headers = {}
def reverse_url(self, scheme, path):
"""
Reverses the url using self.url and path given in parameter.
Reverses the url using scheme and path given in parameter.
Arguments:
- `path`: the request path
:param str scheme: Scheme of the url
:param str path: Path of the url
:return: str
"""
server, port = self.connection_handler.server, self.connection_handler.port
......@@ -87,113 +143,53 @@ class API(object):
module=self.module)
return url + path
def get(self, session, **kwargs):
"""wrapper of overloaded __get__ method."""
return self.__get__(session, **kwargs)
def post(self, session, **kwargs):
"""wrapper of overloaded __post__ method."""
logger.debug('do some work with')
data = self.__post__(session, **kwargs)
logger.debug('and send back')
return data
async def __get__(self, session, **kwargs):
"""interface purpose for GET request"""
pass
async def __post__(self, session, **kwargs):
"""interface purpose for POST request"""
pass
def parse_text(self, text):
"""
Validate and parse the BMA answer from websocket
:param str text: the bma answer
:return: the json data
"""
try:
data = json.loads(text)
jsonschema.validate(data, self.schema)
return data
except (TypeError, json.decoder.JSONDecodeError):
raise jsonschema.ValidationError("Could not parse json")
def parse_error(self, text):
"""
Validate and parse the BMA answer from websocket
:param str text: the bma error
:return: the json data
"""
try:
data = json.loads(text)
jsonschema.validate(data, self.error_schema)
return data
except (TypeError, json.decoder.JSONDecodeError):
raise jsonschema.ValidationError("Could not parse json")
async def parse_response(self, response):
"""
Validate and parse the BMA answer
:param response:
:return: the json data
"""
try:
data = await response.json()
jsonschema.validate(data, self.schema)
return data
except (TypeError, json.decoder.JSONDecodeError):
raise jsonschema.ValidationError("Could not parse json")
async def requests_get(self, session, path, **kwargs):
async def requests_get(self, path, **kwargs):
"""
Requests GET wrapper in order to use API parameters.
:params aiohttp.ClientSession session: the client session
:params str path: the request path
:param str path: the request path
:rtype: aiohttp.ClientResponse
"""
logging.debug("Request : {0}".format(self.reverse_url("http", path)))
logging.debug("Request : {0}".format(self.reverse_url(self.connection_handler.http_scheme, path)))
with aiohttp.Timeout(15):
response = await session.get(self.reverse_url("http", path), params=kwargs,headers=self.headers)
url = self.reverse_url(self.connection_handler.http_scheme, path)
response = await self.connection_handler.session.get(url, params=kwargs, headers=self.headers,
proxy=self.connection_handler.proxy)
if response.status != 200:
try:
error_data = self.parse_error(await response.text())
error_data = parse_error(await response.text())
raise DuniterError(error_data)
except TypeError:
raise ValueError('status code != 200 => %d (%s)' % (response.status, (await response.text())))
return response
async def requests_post(self, session, path, **kwargs):
async def requests_post(self, path, **kwargs):
"""
Requests POST wrapper in order to use API parameters.
:param aiohttp.ClientSession session: the request session
:param str path: the request path
:rtype: aiohttp.ClientResponse
"""
if 'self_' in kwargs:
kwargs['self'] = kwargs.pop('self_')
logging.debug("POST : {0}".format(kwargs))
with aiohttp.Timeout(15):
response = await session.post(self.reverse_url("http", path), data=kwargs, headers=self.headers)
response = await self.connection_handler.session.post(
self.reverse_url(self.connection_handler.http_scheme, path),
data=kwargs,
headers=self.headers,
proxy=self.connection_handler.proxy
)
return response
def connect_ws(self, session, path):
def connect_ws(self, path):
"""
Connect to a websocket in order to use API parameters
:param aiohttp.ClientSession session: the session of the connection
:param str path: the url path
:return:
:rtype: aiohttp.ClientWebSocketResponse
"""
url = self.reverse_url("ws", path)
return session.ws_connect(url)
url = self.reverse_url(self.connection_handler.ws_scheme, path)
return self.connection_handler.session.ws_connect(url, proxy=self.connection_handler.proxy)
This diff is collapsed.
......@@ -16,19 +16,38 @@
# Caner Candan <caner@candan.fr>, http://caner.candan.fr
#
from duniterpy.api.bma.network import Network, logging
from duniterpy.api.bma import API, logging, parse_response
logger = logging.getLogger("duniter/network/peering")
logger = logging.getLogger("duniter/network")
URL_PATH = 'network'
class Base(Network):
def __init__(self, connection_handler):
super(Base, self).__init__(connection_handler, 'network/peering')
PEERING_SCHEMA = {
"type": "object",
"properties": {
"version": {
"type": ["number", "string"]
},
"currency": {
"type": "string"
},
"pubkey": {
"type": "string"
},
"endpoints": {
"type": "array",
"items": {
"type": "string"
}
},
"signature": {
"type": "string"
}
},
"required": ["version", "currency", "pubkey", "endpoints", "signature"]
}
class Peers(Base):
"""GET peering entries of every node inside the currency network."""
schema = {
PEERS_SCHEMA = schema = {
"type": ["object"],
"properties": {
"depth": {
......@@ -81,26 +100,64 @@ class Peers(Base):
]
}
async def __get__(self, session, **kwargs):
"""creates a generator with one peering entry per iteration."""
async def peering(connection):
"""
GET peering information about a peer
:param duniterpy.api.bma.ConnectionHandler connection: Connection handler instance
:rtype: dict
"""
r = await self.requests_get(session, '/peers', **kwargs)
return (await self.parse_response(r))
client = API(connection, URL_PATH)
r = await client.requests_get('/peering')
return await parse_response(r, PEERING_SCHEMA)
async def __post__(self, session, **kwargs):
assert 'entry' in kwargs
assert 'signature' in kwargs
async def peers(connection, leaves=False, leaf=""):
"""
GET peering entries of every node inside the currency network
r = await self.requests_post(session, '/peers', **kwargs)
return r
:param bool leaves: True if leaves should be requested
:param str leaf: True if leaf should be requested
:rtype: dict
"""
client = API(connection, URL_PATH)
# GET Peers
if leaves:
r = await client.requests_get('/peering/peers', leaves=leaves)
else:
r = await client.requests_get('/peering/peers', leaf=leaf)
class Status(Base):
"""POST a network status document to this node in order notify of its status."""
return await parse_response(r, PEERS_SCHEMA)
async def __post__(self, session, **kwargs):
assert 'status' in kwargs
assert 'signature' in kwargs
async def peer(connection, entry=None, signature=None):
"""
GET peering entries of every node inside the currency network
r = await self.requests_post(session, '/status', **kwargs)
return r
:param duniterpy.api.bma.ConnectionHandler connection: Connection handler instance
:param duniterpy.documents.peer.Peer entry: Peer document
:param str signature: Signature of the document issuer
:rtype: dict
"""
client = API(connection, URL_PATH)
r = await client.requests_post('/peering/peers', entry=entry, signature=signature)
return r
# async def status(connection):
# """
# NOT DOCUMENTED IN BMA API DOCUMENTATION
# POST a network status document to this node in order notify of its status
#
# :param duniterpy.api.bma.ConnectionHandler connection: Connection handler instance
# :param duniterpy.documents.peer.Peer entry: Peer document
# :param str signature: Signature of the document issuer
# :rtype: dict
# """
#
# async def __post__(self, session, **kwargs):
# assert 'status' in kwargs
# assert 'signature' in kwargs
#
# r = await self.requests_post(session, '/status', **kwargs)
# return r
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# Authors:
# Caner Candan <caner@candan.fr>, http://caner.candan.fr
#
from .. import API, logging
logger = logging.getLogger("duniter/network")
class Network(API):
def __init__(self, connection_handler, module='network'):
super(Network, self).__init__(connection_handler, module)
class Peering(Network):
"""GET peering information about a peer."""
schema = {
"type": "object",
"properties": {
"version": {
"type": ["number", "string"]
},
"currency": {
"type": "string"
},
"pubkey": {
"type": "string"
},
"endpoints": {
"type": "array",
"items": {
"type": "string"
}
},
"signature": {
"type": "string"
}
},
"required": ["version", "currency", "pubkey", "endpoints", "signature"]
}
async def __get__(self, session, **kwargs):
r = await self.requests_get(session, '/peering', **kwargs)
return (await self.parse_response(r))
from . import peering
......@@ -16,18 +16,19 @@
# Caner Candan <caner@candan.fr>, http://caner.candan.fr
#
from duniterpy.api.bma import API, logging
from duniterpy.api.bma import API, logging, parse_response
logger = logging.getLogger("duniter/node")
URL_PATH = 'node'
class Node(API):
def __init__(self, connection_handler, module='node'):
super(Node, self).__init__(connection_handler, module)
async def summary(connection):
"""
GET Certification data over a member
class Summary(Node):
"""GET Certification data over a member."""
:param duniterpy.api.bma.ConnectionHandler connection: Connection handler instance
:rtype: dict
"""
schema = {
"type": "object",
"properties": {
......@@ -49,11 +50,7 @@ class Summary(Node):
},
"required": ["duniter"]
}
client = API(connection, URL_PATH)
def __init__(self, connection_handler, module='node'):
super(Summary, self).__init__(connection_handler, module)
async def __get__(self, session, **kwargs):
r = await self.requests_get(session, '/summary', **kwargs)
return await self.parse_response(r)
r = await client.requests_get('/summary')
return await parse_response(r, schema)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# Authors:
# Caner Candan <caner@candan.fr>, http://caner.candan.fr
#
from duniterpy.api.bma import API, logging, parse_response
logger = logging.getLogger("duniter/tx")
URL_PATH = 'tx'
HISTORY_SCHEMA = {
"type": "object",
"properties": {
"currency": {
"type": "string"
},
"pubkey": {
"type": "string"
},
"history": {
"type": "object",
"properties": {
"sent": {
"$ref": "#/definitions/transaction_data"
},
"received": {
"$ref": "#/definitions/transaction_data"
},
"sending": {
"$ref": "#/definitions/transactioning_data"
},
"receiving": {
"$ref": "#/definitions/transactioning_data"
},
},
"required": ["sent", "received", "sending", "receiving"]
}
},
"definitions": {
"transaction_data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"version": {
"type": "number"
},
"issuers": {
"type": "array",
"items": {
"type": "string"
}
},
"inputs": {
"type": "array",
"items": {
"type": "string"
}
},
"outputs": {
"type": "array",
"items": {
"type": "string"
}
},
"unlocks": {
"type": "array",
"items": {
"type": "string"
}
},
"comment": {
"type": "string"
},
"signatures": {
"type": "array",
"items": {
"type": "string"
}
},
"hash": {
"type": "string"
},
"block_number": {
"type": "number"
},
"time": {
"type": "number"
}
},
"required": ["version", "issuers", "inputs", "outputs",
"comment", "signatures", "hash", "block_number", "time"]
}
},
"transactioning_data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"version": {
"type": "number"
},
"issuers": {
"type": "array",
"items": {
"type": "string"
}
},
"inputs": {
"type": "array",
"items": {
"type": "string"
}
},
"outputs": {
"type": "array",
"items": {
"type": "string"
}
},
"unlocks": {
"type": "array",
"items": {
"type": "string"
}
},
"comment": {
"type": "string"
},
"signatures": {
"type": "array",
"items": {
"type": "string"
}
},
"hash": {
"type": "string"
},
},
"required": ["version", "issuers", "inputs", "outputs",
"comment", "signatures", "hash"]
}
}
},
"required": ["currency", "pubkey", "history"]
}
SOURCES_SCHEMA = {
"type": "object",
"properties": {
"currency": {
"type": "string"
},
"pubkey": {
"type": "string"
},
"sources": {
"type": "array",
"items": {
"type": "object",
"properties": {
"type": {
"type": "string"
},
"noffset": {
"type": "number"
},
"identifier": {
"type": "string"
},
"amount": {
"type": "number"
},
"base": {