diff --git a/requirements.txt b/requirements.txt index 145df061d3a4c6fa1c9cb843d1b39244403a12bf..0fadd5f310bf76e3db271b6a48a68a90397caef7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ -duniterpy>=0.20.dev0 git+https://github.com/Insoleet/quamash.git@master asynctest -networkx \ No newline at end of file +networkx +git+https://github.com/hynek/attrs.git@master +git+https://github.com/duniter/duniter-python-api.git@master diff --git a/src/sakia/data/entities/__init__.py b/src/sakia/data/entities/__init__.py index 73cc2df312598e04f93841a05d05e00d473d6248..a6ebca41552f7d64cd933e4c2f6a1e874dbb8c75 100644 --- a/src/sakia/data/entities/__init__.py +++ b/src/sakia/data/entities/__init__.py @@ -1 +1,2 @@ -from .identity import Identity \ No newline at end of file +from .identity import Identity +from .community import Community diff --git a/src/sakia/data/entities/blockchain.py b/src/sakia/data/entities/blockchain.py new file mode 100644 index 0000000000000000000000000000000000000000..62e5b495947f0c6cb627a272cd2a2d32c2b94533 --- /dev/null +++ b/src/sakia/data/entities/blockchain.py @@ -0,0 +1,14 @@ +import attr +from duniterpy.documents import block_uid, BlockUID + + +@attr.s() +class Blockchain: + current_buid = attr.ib(convert=block_uid, default=BlockUID.empty()) + nb_members = attr.ib(convert=int, default=0, cmp=False) + current_mass = attr.ib(convert=int, default=0, cmp=False) + median_time = attr.ib(convert=int, default=0, cmp=False) + last_ud = attr.ib(convert=int, default=0, cmp=False) + last_ud_base = attr.ib(convert=int, default=0, cmp=False) + previous_mass = attr.ib(convert=int, default=0, cmp=False) + currency = attr.ib(convert=str, default="", cmp=False) diff --git a/src/sakia/data/entities/community.py b/src/sakia/data/entities/community.py new file mode 100644 index 0000000000000000000000000000000000000000..2e483c091e0286046873def1bb908a573c12b56a --- /dev/null +++ b/src/sakia/data/entities/community.py @@ -0,0 +1,42 @@ +import attr + + +@attr.s() +class Community: + # The decimal percent growth of the UD every [dt] period + c = attr.ib(convert=float, default=0, cmp=False) + # Time period between two UD in seconds + dt = attr.ib(convert=int, default=0, cmp=False) + # UD(0), i.e. initial Universal Dividend + ud0 = attr.ib(convert=int, default=0, cmp=False) + # Minimum delay between 2 certifications of a same issuer, in seconds. Must be positive or zero + sig_period = attr.ib(convert=int, default=0, cmp=False) + # Maximum quantity of active certifications made by member + sig_stock = attr.ib(convert=int, default=0, cmp=False) + # Maximum delay in seconds a certification can wait before being expired for non-writing + sig_window = attr.ib(convert=int, default=0, cmp=False) + # Maximum age of a active signature (in seconds) + sig_validity = attr.ib(convert=int, default=0, cmp=False) + # Minimum quantity of signatures to be part of the WoT + sig_qty = attr.ib(convert=int, default=0, cmp=False) + # Minimum decimal percent of sentries to reach to match the distance rule + xpercent = attr.ib(convert=float, default=0, cmp=False) + # Maximum age of an active membership( in seconds) + ms_validity = attr.ib(convert=int, default=0, cmp=False) + # Maximum distance between each WoT member and a newcomer + step_max = attr.ib(convert=int, default=0, cmp=False) + # Number of blocks used for calculating median time + median_time_blocks = attr.ib(convert=int, default=0, cmp=False) + # The average time for writing 1 block (wished time) in seconds + avg_gen_time = attr.ib(convert=int, default=0, cmp=False) + # The number of blocks required to evaluate again PoWMin value + dt_diff_eval = attr.ib(convert=int, default=0, cmp=False) + # The number of previous blocks to check for personalized difficulty + blocks_rot = attr.ib(convert=int, default=0, cmp=False) + # The decimal percent of previous issuers to reach for personalized difficulty + percent_rot = attr.ib(convert=float, default=0, cmp=False) + # Currency name + currency = attr.ib(convert=str, default="", cmp=False) + + + diff --git a/src/sakia/data/processors/communities.py b/src/sakia/data/processors/communities.py new file mode 100644 index 0000000000000000000000000000000000000000..b9ad62a4e09e8bff3f4f7d4d2f9187dc139a656f --- /dev/null +++ b/src/sakia/data/processors/communities.py @@ -0,0 +1,16 @@ +import attr + + +@attr.s +class CommunityProcessor: + _repo = attr.ib() # :type sakia.data.repositories.CommunitiesRepo + + async def get_from_currency(self, currency): + """ + Get the community of a currency + + :param currency: + :rtype: sakia.data.entities.Community + """ + return self._repo.get_one(currency=currency) + diff --git a/src/sakia/data/repositories/__init__.py b/src/sakia/data/repositories/__init__.py index ea239c6ee545babb52adb06d72d9432e9f7194d3..eca82cc1959936da34fffaf56cbd9fd921a89ee6 100644 --- a/src/sakia/data/repositories/__init__.py +++ b/src/sakia/data/repositories/__init__.py @@ -1,2 +1,3 @@ from .identities import IdentitiesRepo -from .meta import MetaDatabase \ No newline at end of file +from .communities import CommunitiesRepo +from .meta import MetaDatabase diff --git a/src/sakia/data/repositories/communities.py b/src/sakia/data/repositories/communities.py new file mode 100644 index 0000000000000000000000000000000000000000..d2f21a1b73676c07be21d563da0374172ad1122b --- /dev/null +++ b/src/sakia/data/repositories/communities.py @@ -0,0 +1,102 @@ +import attr + +from ..entities import Community + + +@attr.s(frozen=True) +class CommunitiesRepo: + """The repository for Communities entities. + """ + _conn = attr.ib() # :type sqlite3.Connection + _primary_keys = (Community.currency,) + + def insert(self, community): + """ + Commit a community to the database + :param sakia.data.entities.Community community: the community to commit + """ + with self._conn: + community_tuple = attr.astuple(community) + values = ",".join(['?'] * len(community_tuple)) + self._conn.execute("INSERT INTO communities VALUES ({0})".format(values), community_tuple) + + def update(self, community): + """ + Update an existing community in the database + :param sakia.data.entities.Community community: the community to update + """ + with self._conn: + updated_fields = attr.astuple(community, filter=attr.filters.exclude(*CommunitiesRepo._primary_keys)) + where_fields = attr.astuple(community, filter=attr.filters.include(*CommunitiesRepo._primary_keys)) + self._conn.execute("""UPDATE communities SET + c=?, + dt=?, + ud0=?, + sig_period=?, + sig_stock=?, + sig_window=?, + sig_validity=?, + sig_qty=?, + xpercent=?, + ms_validity=?, + step_max=?, + median_time_blocks=?, + avg_gen_time=?, + dt_diff_eval=?, + blocks_rot=?, + percent_rot=? + WHERE + currency=?""", + updated_fields + where_fields) + + def get_one(self, **search): + """ + Get an existing community in the database + :param dict search: the criterions of the lookup + :rtype: sakia.data.entities.Community + """ + with self._conn: + filters = [] + values = [] + for k, v in search.items(): + filters.append("{k}=?".format(k=k)) + values.append(v) + + request = "SELECT * FROM communities WHERE {filters}".format(filters=" AND ".join(filters)) + + c = self._conn.execute(request, tuple(values)) + data = c.fetchone() + if data: + return Community(*data) + + def get_all(self, **search): + """ + Get all existing community in the database corresponding to the search + :param dict search: the criterions of the lookup + :rtype: sakia.data.entities.Community + """ + with self._conn: + filters = [] + values = [] + for k, v in search.items(): + operator = "LIKE" if k == "currency" else "=" + value = "%{value}%".format(value=v) if k == "currency" else v + filters.append("{key} {operator} ?".format(key=k, operator=operator)) + values.append(value) + + request = "SELECT * FROM communities WHERE {filters}".format(filters=" AND ".join(filters)) + + c = self._conn.execute(request, tuple(values)) + datas = c.fetchall() + if datas: + return [Community(*data) for data in datas] + return [] + + def drop(self, community): + """ + Drop an existing community from the database + :param sakia.data.entities.Community community: the community to update + """ + with self._conn: + where_fields = attr.astuple(community, filter=attr.filters.include(*CommunitiesRepo._primary_keys)) + self._conn.execute("DELETE FROM communities WHERE currency=?", where_fields) diff --git a/src/sakia/data/repositories/identities.py b/src/sakia/data/repositories/identities.py index af15b132748ae6122648db5cbadf9662632a489a..d4ed89ba6df12661813342556e32fa294c6dea96 100644 --- a/src/sakia/data/repositories/identities.py +++ b/src/sakia/data/repositories/identities.py @@ -1,4 +1,5 @@ import attr + from ..entities import Identity @@ -16,9 +17,8 @@ class IdentitiesRepo: """ with self._conn: identity_tuple = attr.astuple(identity) - values = ",".join(['?']*len(identity_tuple)) - self._conn.execute("INSERT INTO identities " - "VALUES ({0})".format(values), identity_tuple) + values = ",".join(['?'] * len(identity_tuple)) + self._conn.execute("INSERT INTO identities VALUES ({0})".format(values), identity_tuple) def update(self, identity): """ @@ -28,20 +28,20 @@ class IdentitiesRepo: with self._conn: updated_fields = attr.astuple(identity, filter=attr.filters.exclude(*IdentitiesRepo._primary_keys)) where_fields = attr.astuple(identity, filter=attr.filters.include(*IdentitiesRepo._primary_keys)) - self._conn.execute("UPDATE identities SET " - "signature=?, " - "ts=?," - "written=?," - "revoked=?," - "member=?," - "ms_buid=?," - "ms_timestamp=?" - "WHERE " - "currency=? AND " - "pubkey=? AND " - "uid=? AND " - "blockstamp=?", updated_fields + where_fields - ) + self._conn.execute("""UPDATE identities SET + signature=?, + ts=?, + written=?, + revoked=?, + member=?, + ms_buid=?, + ms_timestamp=? + WHERE + currency=? AND + pubkey=? AND + uid=? AND + blockstamp=?""", updated_fields + where_fields + ) def get_one(self, **search): """ @@ -56,8 +56,7 @@ class IdentitiesRepo: filters.append("{k}=?".format(k=k)) values.append(v) - request = "SELECT * FROM identities WHERE " - request += " AND ".join(filters) + request = "SELECT * FROM identities WHERE {filters}".format(filters=" AND ".join(filters)) c = self._conn.execute(request, tuple(values)) data = c.fetchone() @@ -77,8 +76,7 @@ class IdentitiesRepo: filters.append("{k}=?".format(k=k)) values.append(v) - request = "SELECT * FROM identities WHERE " - request += " AND ".join(filters) + request = "SELECT * FROM identities WHERE {filters}".format(filters=" AND ".join(filters)) c = self._conn.execute(request, tuple(values)) datas = c.fetchall() @@ -93,8 +91,8 @@ class IdentitiesRepo: """ with self._conn: where_fields = attr.astuple(identity, filter=attr.filters.include(*IdentitiesRepo._primary_keys)) - self._conn.execute("DELETE FROM identities WHERE " - "currency=? AND " - "pubkey=? AND " - "uid=? AND " - "blockstamp=?", where_fields) + self._conn.execute("""DELETE FROM identities WHERE + currency=? AND + pubkey=? AND + uid=? AND + blockstamp=?""", where_fields) diff --git a/src/sakia/data/repositories/meta.py b/src/sakia/data/repositories/meta.py index 6e2707d67ec32147d6e5123da16fae7f0291f247..319a03fcb303cced285f0eba4f319bbe050cb0ed 100644 --- a/src/sakia/data/repositories/meta.py +++ b/src/sakia/data/repositories/meta.py @@ -1,4 +1,5 @@ import attr +import os @attr.s(frozen=True) @@ -13,11 +14,11 @@ class MetaDatabase: Prepares the database if the table is missing """ with self._conn: - self._conn.execute("create table if not exists meta(" - "id integer not null," - "version integer not null," - "primary key (id)" - ")" + self._conn.execute("""CREATE TABLE IF NOT EXISTS meta( + id INTEGER NOT NULL, + version INTEGER NOT NULL, + PRIMARY KEY (id) + )""" ) @property @@ -42,22 +43,9 @@ class MetaDatabase: Init all the tables :return: """ + sql_file = open(os.path.join(os.path.dirname(__file__), 'meta.sql'), 'r') with self._conn: - self._conn.execute("create table if not exists identities(" - "currency varchar(30), " - "pubkey varchar(50)," - "uid varchar(255)," - "blockstamp varchar(100)," - "signature varchar(100)," - "ts int," - "written boolean," - "revoked boolean," - "member boolean," - "ms_buid varchar(100)," - "ms_timestamp int," - "PRIMARY KEY (currency, pubkey, uid, blockstamp)" - ")" - ) + self._conn.executescript(sql_file.read()) def version(self): with self._conn: @@ -68,4 +56,3 @@ class MetaDatabase: else: self._conn.execute("INSERT INTO meta VALUES (1, 0)") return 0 - diff --git a/src/sakia/data/repositories/meta.sql b/src/sakia/data/repositories/meta.sql new file mode 100644 index 0000000000000000000000000000000000000000..91f047aca1e81af80dcb137bc3377231464a4c0d --- /dev/null +++ b/src/sakia/data/repositories/meta.sql @@ -0,0 +1,37 @@ +-- IDENTITY TABLE +CREATE TABLE IF NOT EXISTS identities( + currency VARCHAR(30), + pubkey VARCHAR(50), + uid VARCHAR(255), + blockstamp VARCHAR(100), + signature VARCHAR(100), + ts INT, + written BOOLEAN, + revoked BOOLEAN, + member BOOLEAN, + ms_buid VARCHAR(100), + ms_timestamp INT, + PRIMARY KEY (currency, pubkey, uid, blockstamp) + ); + +-- COMMUNITY TABLE +CREATE TABLE IF NOT EXISTS communities( + c FLOAT(1,6), + dt INT, + ud0 INT, + sig_period INT, + sig_stock INT, + sig_window INT, + sig_validity INT, + sig_qty INT, + xpercent FLOAT(1,6), + ms_validity INT, + step_max INT, + median_time_blocks INT, + avg_gen_time INT, + dt_diff_eval INT, + blocks_rot INT, + percent_rot FLOAT(1,6), + currency VARCHAR(30), + PRIMARY KEY (currency) + ); diff --git a/src/sakia/tests/unit/data/test_communities_repo.py b/src/sakia/tests/unit/data/test_communities_repo.py new file mode 100644 index 0000000000000000000000000000000000000000..5a0d72f3b93c751b5b06ed3b865aa1c3ac39227d --- /dev/null +++ b/src/sakia/tests/unit/data/test_communities_repo.py @@ -0,0 +1,132 @@ +import sqlite3 +import unittest + +from duniterpy.documents import BlockUID + +from sakia.data.entities import Community +from sakia.data.repositories import CommunitiesRepo, MetaDatabase + + +class TestIdentitiesRepo(unittest.TestCase): + def setUp(self): + sqlite3.register_adapter(BlockUID, str) + sqlite3.register_adapter(bool, int) + sqlite3.register_converter("BOOLEAN", lambda v: bool(int(v))) + self.con = sqlite3.connect(":memory:", detect_types=sqlite3.PARSE_DECLTYPES) + + def tearDown(self): + self.con.close() + + def test_add_get_drop_community(self): + meta_repo = MetaDatabase(self.con) + meta_repo.prepare() + meta_repo.upgrade_database() + communities_repo = CommunitiesRepo(self.con) + communities_repo.insert(Community( + 0.1, + 86400, + 100000, + 10800, + 40, + 2629800, + 31557600, + 1, + 0.9, + 604800, + 5, + 12, + 300, + 25, + 10, + 0.66, + "testcurrency" + )) + community = communities_repo.get_one(currency="testcurrency") + self.assertEqual(community.currency, "testcurrency") + self.assertEqual(community.c, 0.1) + self.assertEqual(community.dt, 86400) + + communities_repo.drop(community) + community = communities_repo.get_one(currency="testcurrency") + self.assertIsNone(community) + + def test_add_get_multiple_community(self): + meta_repo = MetaDatabase(self.con) + meta_repo.prepare() + meta_repo.upgrade_database() + communities_repo = CommunitiesRepo(self.con) + communities_repo.insert(Community( + 0.1, + 86400, + 100000, + 10800, + 40, + 2629800, + 31557600, + 1, + 0.9, + 604800, + 5, + 12, + 300, + 25, + 10, + 0.66, + "testcurrency" + ) + ) + communities_repo.insert(Community( + 0.1, + 86400 * 365, + 100000, + 10800, + 40, + 2629800, + 31557600, + 1, + 0.9, + 604800, + 5, + 12, + 300, + 25, + 10, + 0.66, + "testcurrency2" + ) + ) + communities = communities_repo.get_all(currency="testcurrency") + self.assertIn("testcurrency", [i.currency for i in communities]) + self.assertIn("testcurrency2", [i.currency for i in communities]) + self.assertIn(86400, [i.dt for i in communities]) + self.assertIn(86400 * 365, [i.dt for i in communities]) + + def test_add_update_community(self): + meta_repo = MetaDatabase(self.con) + meta_repo.prepare() + meta_repo.upgrade_database() + communities_repo = CommunitiesRepo(self.con) + community = Community( + 0.1, + 86400, + 100000, + 10800, + 40, + 2629800, + 31557600, + 1, + 0.9, + 604800, + 5, + 12, + 300, + 25, + 10, + 0.66, + "testcurrency" + ) + communities_repo.insert(community) + community.c = 0.0922 + communities_repo.update(community) + community2 = communities_repo.get_one(currency="testcurrency") + self.assertEquals(0.0922, community2.c)