[DEV] some upgrade

This commit is contained in:
Edouard DUPIN 2023-01-17 21:41:46 +01:00
parent 95902e7312
commit 1d0e8899d5
13 changed files with 151 additions and 130 deletions

View File

@ -2,10 +2,9 @@ package org.kar.karanage.api;
import org.kar.archidata.SqlWrapper; import org.kar.archidata.SqlWrapper;
import org.kar.archidata.WhereCondition; import org.kar.archidata.WhereCondition;
import org.kar.karanage.model.StateInstant;
import org.kar.karanage.model.DataLog; import org.kar.karanage.model.DataLog;
import org.kar.karanage.model.StateHistory;
import org.kar.karanage.model.Group; import org.kar.karanage.model.Group;
import org.kar.karanage.model.MultipleLogElement;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
@ -18,8 +17,6 @@ import javax.ws.rs.core.MediaType;
import java.sql.Timestamp; import java.sql.Timestamp;
import java.time.Instant; import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.List; import java.util.List;
@ -135,7 +132,8 @@ public class LogResource {
@PathParam("group") String groupName, @PathParam("group") String groupName,
@PathParam("system") String system, @PathParam("system") String system,
@QueryParam("time") String time, @QueryParam("time") String time,
@QueryParam("id") Integer client_id, @QueryParam("id") Long clientId,
@QueryParam("uuid") Long clientUuid,
String dataToInsert) throws Exception { String dataToInsert) throws Exception {
Group group = SqlWrapper.getWhere(Group.class, Group group = SqlWrapper.getWhere(Group.class,
List.of( List.of(
@ -149,8 +147,11 @@ public class LogResource {
data.group = group.id; data.group = group.id;
data.system = system; data.system = system;
data.data = dataToInsert; data.data = dataToInsert;
if (client_id != null) { if (clientId != null) {
data.client_id = client_id; data.clientId = clientId;
}
if (clientUuid != null) {
data.clientUuid = clientUuid;
} }
if (time != null) { if (time != null) {
try { try {
@ -162,6 +163,46 @@ public class LogResource {
SqlWrapper.insert(data); SqlWrapper.insert(data);
return Response.status(201).build(); return Response.status(201).build();
} }
@POST
@Path("{group}/{system}/push_multiple")
@PermitAll
@Consumes(MediaType.APPLICATION_JSON)
public Response post(
@PathParam("group") String groupName,
@PathParam("system") String system,
@QueryParam("uuid") Long clientUuid,
List<MultipleLogElement> listDataToInsert) throws Exception {
Group group = SqlWrapper.getWhere(Group.class,
List.of(
new WhereCondition("name", "=", groupName)
),
false);
if (group == null) {
throw new InputException("group", "url: /log/{group}/... ==> Unknown group name");
}
for (MultipleLogElement dataToInsert : listDataToInsert) {
DataLog data = new DataLog();
data.group = group.id;
data.system = system;
data.data = dataToInsert.data;
if (dataToInsert.id != null) {
data.clientId = dataToInsert.id;
}
if (clientUuid != null) {
data.clientUuid = clientUuid;
}
if (dataToInsert.time != null) {
try {
data.create_date = Timestamp.from(Instant.parse(dataToInsert.time));
} catch (Exception ex) {
throw new InputException("time", "url: ?time=... ==> is not an iso 8601 time format");
}
}
// TODO: Do a single insertion for multiple DATAS...
SqlWrapper.insert(data);
}
return Response.status(201).build();
}
} }

View File

@ -17,8 +17,6 @@ import javax.ws.rs.core.MediaType;
import java.sql.Timestamp; import java.sql.Timestamp;
import java.time.Instant; import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.List; import java.util.List;

View File

@ -1,16 +1,4 @@
package org.kar.karanage.model; package org.kar.karanage.model;
/*
CREATE TABLE `node` (
`id` bigint NOT NULL COMMENT 'table ID' AUTO_INCREMENT PRIMARY KEY,
`deleted` BOOLEAN NOT NULL DEFAULT false,
`create_date` datetime NOT NULL DEFAULT now() COMMENT 'Time the element has been created',
`modify_date` datetime NOT NULL DEFAULT now() COMMENT 'Time the element has been update',
`type` enum("TYPE", "UNIVERS", "SERIE", "SAISON", "MEDIA") NOT NULL DEFAULT 'TYPE',
`name` TEXT COLLATE 'utf8_general_ci' NOT NULL,
`description` TEXT COLLATE 'utf8_general_ci',
`parent_id` bigint
) AUTO_INCREMENT=10;
*/
import org.kar.archidata.annotation.SQLComment; import org.kar.archidata.annotation.SQLComment;
import org.kar.archidata.annotation.SQLForeignKey; import org.kar.archidata.annotation.SQLForeignKey;
@ -19,11 +7,11 @@ import org.kar.archidata.annotation.SQLLimitSize;
import org.kar.archidata.annotation.SQLTableName; import org.kar.archidata.annotation.SQLTableName;
import org.kar.archidata.model.GenericTable; import org.kar.archidata.model.GenericTable;
import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.annotation.JsonInclude;
@SQLTableName ("applicationToken") @SQLTableName ("applicationToken")
@SQLIfNotExists @SQLIfNotExists
@JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL) @JsonInclude(JsonInclude.Include.NON_NULL)
public class ApplicationToken extends GenericTable { public class ApplicationToken extends GenericTable {
@SQLComment("Group of the element") @SQLComment("Group of the element")
@SQLForeignKey("group") @SQLForeignKey("group")

View File

@ -9,16 +9,16 @@ import org.kar.archidata.annotation.SQLForeignKey;
import org.kar.archidata.annotation.SQLIfNotExists; import org.kar.archidata.annotation.SQLIfNotExists;
import org.kar.archidata.annotation.SQLLimitSize; import org.kar.archidata.annotation.SQLLimitSize;
import org.kar.archidata.annotation.SQLNotNull; import org.kar.archidata.annotation.SQLNotNull;
import org.kar.archidata.annotation.SQLNotRead;
import org.kar.archidata.annotation.SQLPrimaryKey; import org.kar.archidata.annotation.SQLPrimaryKey;
import org.kar.archidata.annotation.SQLTableName; import org.kar.archidata.annotation.SQLTableName;
import org.kar.archidata.annotation.SQLUpdateTime; import org.kar.archidata.annotation.SQLUpdateTime;
import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.annotation.JsonInclude;
@SQLTableName ("log") @SQLTableName ("log")
@SQLIfNotExists @SQLIfNotExists
@JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL) @JsonInclude(JsonInclude.Include.NON_NULL)
public class DataLog { public class DataLog {
@SQLAutoIncrement // Add AUTO_INCREMENT modifier @SQLAutoIncrement // Add AUTO_INCREMENT modifier
@SQLPrimaryKey // Create a PRIMARY KEY based on this field @SQLPrimaryKey // Create a PRIMARY KEY based on this field
@ -33,11 +33,15 @@ public class DataLog {
@SQLComment("Group of the element") @SQLComment("Group of the element")
@SQLForeignKey("group") @SQLForeignKey("group")
public long group; public long group;
// TODO: use external system ID table ==> simplify the table complexity
@SQLLimitSize(256) @SQLLimitSize(256)
@SQLComment("System of the message") @SQLComment("System of the message")
public String system = null; public String system = null;
@SQLComment("Client unique internal ID (for synchronisation system)") @SQLComment("Client internal UUID")
public Integer client_id = null; public Long clientUuid;
@SQLComment("Client unique internal ID (for synchronization system)")
public Long clientId = null;
@SQLComment("Message to store") @SQLComment("Message to store")
@SQLNotNull
public String data = null; public String data = null;
} }

View File

@ -7,12 +7,11 @@ import org.kar.archidata.annotation.SQLNotNull;
import org.kar.archidata.annotation.SQLTableName; import org.kar.archidata.annotation.SQLTableName;
import org.kar.archidata.model.GenericTable; import org.kar.archidata.model.GenericTable;
import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.annotation.JsonInclude;
@SQLTableName ("group") @SQLTableName ("group")
@SQLIfNotExists @SQLIfNotExists
@JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL) @JsonInclude(JsonInclude.Include.NON_NULL)
public class Group extends GenericTable { public class Group extends GenericTable {
@SQLNotNull @SQLNotNull
@SQLLimitSize(256) @SQLLimitSize(256)

View File

@ -0,0 +1,7 @@
package org.kar.karanage.model;
public class MultipleLogElement {
public Long id;
public String time;
public String data;
}

View File

@ -1,36 +1,17 @@
package org.kar.karanage.model; package org.kar.karanage.model;
/*
CREATE TABLE `node` (
`id` bigint NOT NULL COMMENT 'table ID' AUTO_INCREMENT PRIMARY KEY,
`deleted` BOOLEAN NOT NULL DEFAULT false,
`create_date` datetime NOT NULL DEFAULT now() COMMENT 'Time the element has been created',
`modify_date` datetime NOT NULL DEFAULT now() COMMENT 'Time the element has been update',
`type` enum("TYPE", "UNIVERS", "SERIE", "SAISON", "MEDIA") NOT NULL DEFAULT 'TYPE',
`name` TEXT COLLATE 'utf8_general_ci' NOT NULL,
`description` TEXT COLLATE 'utf8_general_ci',
`parent_id` bigint
) AUTO_INCREMENT=10;
*/
import java.sql.Timestamp;
import org.kar.archidata.annotation.SQLAutoIncrement;
import org.kar.archidata.annotation.SQLComment; import org.kar.archidata.annotation.SQLComment;
import org.kar.archidata.annotation.SQLDefault; import org.kar.archidata.annotation.SQLDefault;
import org.kar.archidata.annotation.SQLForeignKey;
import org.kar.archidata.annotation.SQLIfNotExists; import org.kar.archidata.annotation.SQLIfNotExists;
import org.kar.archidata.annotation.SQLLimitSize;
import org.kar.archidata.annotation.SQLNotNull; import org.kar.archidata.annotation.SQLNotNull;
import org.kar.archidata.annotation.SQLNotRead;
import org.kar.archidata.annotation.SQLPrimaryKey;
import org.kar.archidata.annotation.SQLTableName; import org.kar.archidata.annotation.SQLTableName;
import org.kar.archidata.annotation.SQLUpdateTime;
import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
enum State { enum State {
OK, FAIL, UNSTABLE, RETREIVING, HAPPY, FULL, EMPTY OK, FAIL, UNSTABLE, RETRIEVING, HAPPY, FULL, EMPTY
} }
enum TypeData { enum TypeData {
NUMBER, BOOLEAN, JSON, STRING NUMBER, BOOLEAN, JSON, STRING
@ -38,7 +19,7 @@ enum TypeData {
@SQLTableName ("stateHistory") @SQLTableName ("stateHistory")
@SQLIfNotExists @SQLIfNotExists
@JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL) @JsonInclude(Include.NON_NULL)
public class StateHistory extends StateInstant { public class StateHistory extends StateInstant {
// default constructor // default constructor
public StateHistory() { public StateHistory() {

View File

@ -1,16 +1,4 @@
package org.kar.karanage.model; package org.kar.karanage.model;
/*
CREATE TABLE `node` (
`id` bigint NOT NULL COMMENT 'table ID' AUTO_INCREMENT PRIMARY KEY,
`deleted` BOOLEAN NOT NULL DEFAULT false,
`create_date` datetime NOT NULL DEFAULT now() COMMENT 'Time the element has been created',
`modify_date` datetime NOT NULL DEFAULT now() COMMENT 'Time the element has been update',
`type` enum("TYPE", "UNIVERS", "SERIE", "SAISON", "MEDIA") NOT NULL DEFAULT 'TYPE',
`name` TEXT COLLATE 'utf8_general_ci' NOT NULL,
`description` TEXT COLLATE 'utf8_general_ci',
`parent_id` bigint
) AUTO_INCREMENT=10;
*/
import java.sql.Timestamp; import java.sql.Timestamp;
@ -20,16 +8,15 @@ import org.kar.archidata.annotation.SQLForeignKey;
import org.kar.archidata.annotation.SQLIfNotExists; import org.kar.archidata.annotation.SQLIfNotExists;
import org.kar.archidata.annotation.SQLLimitSize; import org.kar.archidata.annotation.SQLLimitSize;
import org.kar.archidata.annotation.SQLNotNull; import org.kar.archidata.annotation.SQLNotNull;
import org.kar.archidata.annotation.SQLNotRead;
import org.kar.archidata.annotation.SQLPrimaryKey; import org.kar.archidata.annotation.SQLPrimaryKey;
import org.kar.archidata.annotation.SQLTableName; import org.kar.archidata.annotation.SQLTableName;
import org.kar.archidata.annotation.SQLUpdateTime; import org.kar.archidata.annotation.SQLUpdateTime;
import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.annotation.JsonInclude;
@SQLTableName ("state") @SQLTableName ("state")
@SQLIfNotExists @SQLIfNotExists
@JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL) @JsonInclude(JsonInclude.Include.NON_NULL)
public class StateInstant { public class StateInstant {
@SQLAutoIncrement // Add AUTO_INCREMENT modifier @SQLAutoIncrement // Add AUTO_INCREMENT modifier
@SQLPrimaryKey // Create a PRIMARY KEY based on this field @SQLPrimaryKey // Create a PRIMARY KEY based on this field

View File

@ -4,11 +4,11 @@ import org.kar.archidata.annotation.SQLIfNotExists;
import org.kar.archidata.annotation.SQLTableName; import org.kar.archidata.annotation.SQLTableName;
import org.kar.archidata.model.User; import org.kar.archidata.model.User;
import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.annotation.JsonInclude;
@SQLTableName ("user") @SQLTableName ("user")
@SQLIfNotExists @SQLIfNotExists
@JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL) @JsonInclude(JsonInclude.Include.NON_NULL)
public class UserKaranage extends User { public class UserKaranage extends User {
} }

View File

@ -22,7 +22,7 @@ Just run:
pip3 install karanage pip3 install karanage
``` ```
developpement for KARANAGE: development for KARANAGE:
``` ```
git clone https://gitea.atria-soft.org/kangaroo-and-rabbit/karanage git clone https://gitea.atria-soft.org/kangaroo-and-rabbit/karanage
cd karanage/client/python/karanage cd karanage/client/python/karanage

View File

@ -21,13 +21,12 @@ class KaranageConnection:
token: Optional[str] = None, token: Optional[str] = None,
config_file: Optional[str] = None, config_file: Optional[str] = None,
default_values: Optional[str] = None) -> None: default_values: Optional[str] = None) -> None:
""" """ Initialize the communication class.
@brief Initialize the communication class. :param url: URL of the karanage API server.
@param[in] url URL of the karanage API server. :param group: Group of the message (token need to have the autorotation to published on it).
@param[in] group Group of the message (token need to have the autorisation to pubhied on it). :param token: Token to validate the access on the application.
@param[in] token Token to validate the access on the application. :param config_file: path to the configuration file if overload.
@param[in] config_file path to the configuration file if overload. :param default_values: Default configurations.
@param[in] default_values Default configurations.
""" """
self.url = "http://localhost:20080/karanage/api" self.url = "http://localhost:20080/karanage/api"
self.group = "test" self.group = "test"
@ -46,17 +45,17 @@ class KaranageConnection:
# check if the config exist: # check if the config exist:
if Path(config_file).exists(): if Path(config_file).exists():
f = open(config_file, "r") f = open(config_file, "r")
configuaration = json.loads(f.read()) configuration = json.loads(f.read())
f.close() f.close()
else: else:
configuaration = {} configuration = {}
# Update data with config file: # Update data with config file:
if "url" in configuaration: if "url" in configuration:
self.url = configuaration["url"] self.url = configuration["url"]
if "group" in configuaration: if "group" in configuration:
self.group = configuaration["group"] self.group = configuration["group"]
if "token" in configuaration: if "token" in configuration:
self.token = configuaration["token"] self.token = configuration["token"]
# set user command - line configuration: # set user command - line configuration:
if url is not None: if url is not None:
self.url = url self.url = url

View File

@ -14,12 +14,17 @@ from typing import Dict, Optional
from .connection import KaranageConnection from .connection import KaranageConnection
from .exception import KaranageException from .exception import KaranageException
class GroupLogElement:
def __init__(self, id: int, time: Datetime:None, data: str):
self.id = id
self.time = time
self.data = data
## Generic karanage sending system. ## Generic karanage sending system.
class KaranageLog: class KaranageLog:
def __init__(self, connection: KaranageConnection, system: Optional[str] = None) -> None: def __init__(self, connection: KaranageConnection, system: Optional[str] = None) -> None:
""" """Initialize the communication class.
@brief Initialize the communication class. :param connection: Connection interface.
@param[in] connection Connection interface.
""" """
self.connection = connection self.connection = connection
self.system = system self.system = system
@ -30,23 +35,41 @@ class KaranageLog:
return self.connection.get_url(self.service) return self.connection.get_url(self.service)
return f"{self.connection.get_url(self.service)}/{self.system}" return f"{self.connection.get_url(self.service)}/{self.system}"
def send(self, system: str, data: Dict, id: int = None) -> None: def send(self, data: Dict, id: int = None, uuid_group: int= None, time: Datetime = None) -> None:
""" """Send a message to the server.
@brief Send a message to the server. :param data: Data to send to the server
@param[in] system system where to publish the data. :param id: Local internal ID
@param[in] data: Data to send to the server :param uuid_group: local internal group UUID
@param[in] id: Local internal ID :param time_log: Receive time of the log
""" """
param = {} param = {}
if id is not None: if id is not None:
param["id"] = id param["id"] = id
if uuid_group is not None:
param["uuid"] = uuid_group
if time is not None:
param["time"] = id
header = self.connection.get_header() header = self.connection.get_header()
try: try:
ret = requests.post(self.get_url(), json=data, headers=header, params=param) ret = requests.post(self.get_url(), json=data, headers=header, params=param)
except requests.exceptions.ConnectionError as ex: except requests.exceptions.ConnectionError as ex:
raise KaranageException(f"Fail connect server: {self.get_url('state', system)}", 0, str(ex)) raise KaranageException(f"Fail connect server: {self.get_url()}", 0, str(ex))
if 200 <= ret.status_code <= 299: if not 200 <= ret.status_code <= 299:
pass raise KaranageException(f"Fail send message: {self.get_url()}", ret.status_code, ret.content.decode("utf-8"))
else:
raise KaranageException(f"Fail send message: {self.get_url('state', system)}", ret.status_code, ret.content.decode("utf-8")) def send_multiple(self, data: List[GroupLogElement], uuid_group: int= None) -> None:
"""Send multiple log message to the server.
:param data: Data to send to the server
:param uuid_group: local internal group UUID
"""
param = {}
if uuid_group is not None:
param["uuid"] = uuid_group
header = self.connection.get_header()
try:
ret = requests.post(f"{self.get_url()}/push_multiple", json=data, headers=header, params=param)
except requests.exceptions.ConnectionError as ex:
raise KaranageException(f"Fail connect server: {self.get_url()}", 0, str(ex))
if not 200 <= ret.status_code <= 299:
raise KaranageException(f"Fail send message: {self.get_url()}", ret.status_code, ret.content.decode("utf-8"))

View File

@ -22,9 +22,8 @@ class StateSystem(enum.Enum):
## Generic karanage sending system. ## Generic karanage sending system.
class KaranageState: class KaranageState:
def __init__(self, connection: KaranageConnection) -> None: def __init__(self, connection: KaranageConnection) -> None:
""" """Initialize the communication class.
@brief Initialize the communication class. :param connection: Connection interface.
@param[in] connection Connection interface.
""" """
self.connection = connection self.connection = connection
@ -34,33 +33,29 @@ class KaranageState:
return f"{self.connection.get_url(service)}/{topic}" return f"{self.connection.get_url(service)}/{topic}"
def send(self, topic: str, data: Optional[Dict], state: StateSystem = StateSystem.OK) -> None: def send(self, topic: str, data: Optional[Dict], state: StateSystem = StateSystem.OK) -> None:
""" """Send a message to the server.
@brief Send a message to the server. :param topic: Topic where to publish the data.
@param[in] topic Topic where to publish the data. :param data: Data to send to the server
@param[in] data: Data to send to the server :param state: State of the current system
@param[in] state: State of the current system
""" """
if data is None: if data is None:
data = {} data = {}
param = {} param = {}
if state is not None: if state is not None:
param["state"] = state param["state"] = state.value
header = self.connection.get_header() header = self.connection.get_header()
try: try:
ret = requests.post(self.get_url("state", topic), json=data, headers=header, params=param) ret = requests.post(self.get_url("state", topic), json=data, headers=header, params=param)
except requests.exceptions.ConnectionError as ex: except requests.exceptions.ConnectionError as ex:
raise KaranageException(f"Fail connect server: {self.get_url('state', topic)}", 0, str(ex)) raise KaranageException(f"Fail connect server: {self.get_url('state', topic)}", 0, str(ex))
if 200 <= ret.status_code <= 299: if not 200 <= ret.status_code <= 299:
pass
else:
raise KaranageException(f"Fail send message: {self.get_url('state', topic)}", ret.status_code, ret.content.decode("utf-8")) raise KaranageException(f"Fail send message: {self.get_url('state', topic)}", ret.status_code, ret.content.decode("utf-8"))
def gets(self, topic: Optional[str] = None, since: Optional[str] = None) -> Dict: def gets(self, topic: Optional[str] = None, since: Optional[str] = None) -> Dict:
""" """Get all the topic fom the server.
@brief Get all the topic fom the server. :param since: ISO1866 time value.
@param since ISO1866 time value. :return: A dictionary with the requested data.
@return A dictionnary with the requested data.
""" """
param = { } param = { }
header = self.connection.get_header() header = self.connection.get_header()
@ -68,17 +63,16 @@ class KaranageState:
param["since"] = since param["since"] = since
ret = requests.get(self.get_url("state", topic), headers=header, params=param) ret = requests.get(self.get_url("state", topic), headers=header, params=param)
#print(ret.content.decode('utf-8')) #print(ret.content.decode('utf-8'))
if 200 == ret.status_code: if 200 <= ret.status_code <= 299:
return json.loads(ret.content.decode('utf-8')) return json.loads(ret.content.decode('utf-8'))
raise KaranageException(f"Fail get data: {self.get_url('state', topic)}", ret.status_code, ret.content.decode("utf-8")) raise KaranageException(f"Fail get data: {self.get_url('state', topic)}", ret.status_code, ret.content.decode("utf-8"))
def get_history(self, topic: Optional[str] = None, since: Optional[str] = None, since_id: Optional[int] = None, limit: Optional[int] = None) -> Dict: def get_history(self, topic: Optional[str] = None, since: Optional[str] = None, since_id: Optional[int] = None, limit: Optional[int] = None) -> Dict:
""" """Get all the topic fom the server.
@brief Get all the topic fom the server. :param since: ISO1866 time value.
@param since ISO1866 time value. :param since_id: remote BDD index of the field.
@param since_id remote BDD index of tje fielf. :param limit: the number of value we want to get
@param limit Number of value we want to get :return: A dictionary with the requested data.
@return A dictionnary with the requested data.
""" """
param = { } param = { }
header = self.connection.get_header() header = self.connection.get_header()
@ -90,7 +84,7 @@ class KaranageState:
param["limit"] = limit param["limit"] = limit
ret = requests.get(self.get_url("state_history", topic), headers=header, params=param) ret = requests.get(self.get_url("state_history", topic), headers=header, params=param)
#print(ret.content.decode('utf-8')) #print(ret.content.decode('utf-8'))
if 200 == ret.status_code: if 200 <= ret.status_code <= 299:
return json.loads(ret.content.decode('utf-8')) return json.loads(ret.content.decode('utf-8'))
raise KaranageException(f"Fail get data: {self.get_url('state_history', topic)}", ret.status_code, ret.content.decode("utf-8")) raise KaranageException(f"Fail get data: {self.get_url('state_history', topic)}", ret.status_code, ret.content.decode("utf-8"))