diff --git a/README.md b/README.md index 74aedf6d8c470e55f18bc1beef92ae31dcdbe99c..29fcd18dbe80b6eff3e19eb318abdb8fb7e29482 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ Silkaj is based on following Python modules: - [Click](https://click.palletsprojects.com/): Composable command line interface toolkit - [DuniterPy](https://git.duniter.org/clients/python/duniterpy/): Most complete client oriented Python library for Duniter/Ğ1 ecosystem -- [Arrow](https://arrow.readthedocs.io/): Better dates & times for Python +- [Pendulum](https://pendulum.eustace.io/): Datetimes made easy - [texttable](https://github.com/foutaise/texttable/): Creation of simple ASCII tables ### Names diff --git a/docs/contributing/index.md b/docs/contributing/index.md index 06c6221f1defb377fde5c529fb6b1003319b4ca7..6a3ed6e74fb502fe01957da175c4bdc4ac1b2c58 100644 --- a/docs/contributing/index.md +++ b/docs/contributing/index.md @@ -52,7 +52,7 @@ Please read their documentations on how to use them the best possible. - Feel free to contribute upstream to share the code with other Python programs - [Click](https://click.palletsprojects.com/#documentation) - [Rich-Click](https://github.com/ewels/rich-click) -- [Arrow](https://arrow.readthedocs.io/) +- [Pendulum](https://pendulum.eustace.io/docs/) - [texttable](https://github.com/foutaise/texttable/#documentation) ## Pre-commit hooks diff --git a/pyproject.toml b/pyproject.toml index 32804676e1db8a0756c9aa049edf131534495709..1846302548a0469fb4ede927620ca93ecc4076cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ deathreaper = [ "pydiscourse (>=1.7.0,<2.0)" ] duniterpy = "~1.2.1" rich-click = "^1.8.8" texttable = "^1.7.0" -arrow = "^1.3.0" +pendulum = "^3.1.0" [tool.poetry.group.test.dependencies] pytest = "^8.3.5" diff --git a/silkaj/blockchain/blocks.py b/silkaj/blockchain/blocks.py index d49e4fa9cd1d3b7f54977803c96d54330459fdce..47cacb21f5b85990e5584e54d492f422a15c728f 100644 --- a/silkaj/blockchain/blocks.py +++ b/silkaj/blockchain/blocks.py @@ -17,7 +17,7 @@ import time from operator import itemgetter from urllib.error import HTTPError -import arrow +import pendulum import rich_click as click from duniterpy.api import bma @@ -50,10 +50,12 @@ def list_blocks(number: int, detailed: bool) -> None: issuer["pubkey"] = block["issuer"] if detailed or number <= 30: issuer["block"] = block["number"] - issuer["gentime"] = arrow.get(block["time"]).to("local").format(ALL) - issuer["mediantime"] = ( - arrow.get(block["medianTime"]).to("local").format(ALL) - ) + issuer["gentime"] = pendulum.from_timestamp( + block["time"], tz="local" + ).format(ALL) + issuer["mediantime"] = pendulum.from_timestamp( + block["medianTime"], tz="local" + ).format(ALL) issuer["hash"] = block["hash"][:10] issuer["powMin"] = block["powMin"] issuers_dict[issuer["pubkey"]] = issuer diff --git a/silkaj/blockchain/difficulty.py b/silkaj/blockchain/difficulty.py index 4735c9cec0c790ec4ce071cbf99b8db7b01a45bb..fbd2897330f11e3c2bacbc48eafe9981a1ca092d 100644 --- a/silkaj/blockchain/difficulty.py +++ b/silkaj/blockchain/difficulty.py @@ -16,8 +16,8 @@ from operator import itemgetter from os import system -import arrow import jsonschema +import pendulum import rich_click as click from duniterpy.api import bma from duniterpy.api.client import WSConnection @@ -54,7 +54,7 @@ def display_diffi(current: WSConnection, diffi: dict) -> None: d["Π diffi"] = compute_power(match_pattern(d["level"])[1]) d["Σ diffi"] = d.pop("level") system("cls||clear") - block_gen = arrow.get(current["time"]).to("local").format(ALL) + block_gen = pendulum.from_timestamp(current["time"], tz="local").format(ALL) match = match_pattern(int(current["powMin"]))[0] table = tui.Table(style="columns").set_cols_dtype(["t", "t", "t", "i"]) diff --git a/silkaj/blockchain/information.py b/silkaj/blockchain/information.py index d4d8f503ea4f5fcfb0851eb8a480184488f422a5..225433bba5cda498fc4d1e7f0e11097b6125a52c 100644 --- a/silkaj/blockchain/information.py +++ b/silkaj/blockchain/information.py @@ -13,7 +13,7 @@ # You should have received a copy of the GNU Affero General Public License # along with Silkaj. If not, see <https://www.gnu.org/licenses/>. -import arrow +import pendulum import rich_click as click from silkaj.blockchain.tools import get_head_block @@ -26,8 +26,8 @@ from silkaj.tools import get_currency_symbol def currency_info() -> None: head_block = get_head_block() ep = determine_endpoint() - current_time = arrow.get(head_block["time"]).to("local") - mediantime = arrow.get(head_block["medianTime"]).to("local") + current_time = pendulum.from_timestamp(head_block["time"], tz="local") + mediantime = pendulum.from_timestamp(head_block["medianTime"], tz="local") print( "Connected to node:", ep.host, @@ -45,5 +45,5 @@ def currency_info() -> None: "\nMedian time:", mediantime.format(ALL), "\nDifference time:", - current_time - mediantime, + current_time.diff_for_humans(mediantime, True), ) diff --git a/silkaj/constants.py b/silkaj/constants.py index 10b48d9230c803a91659dd1c438f0d8eb5762e50..01a762b02e526f45f1bf72f5a5a482fbb58e6acd 100644 --- a/silkaj/constants.py +++ b/silkaj/constants.py @@ -39,10 +39,10 @@ MINIMAL_RELATIVE_TX_AMOUNT = 1e-6 CENT_MULT_TO_UNIT = 100 SHORT_PUBKEY_SIZE = 8 -# Arrow date time formats -# https://arrow.readthedocs.io/en/latest/guide.html#supported-tokens -DATE = "MMMM D, YYYY" -ALL = "MMMM D, YYYY hh:mm A ZZZ" +# pendulum constants +# see https://pendulum.eustace.io/docs/#localized-formats +DATE = "LL" +HOUR = "LTS" +ALL = "LLL" # Not ISO 8601 compliant but common ALL_DIGITAL = "YYYY-MM-DD HH:mm:ss" -FULL_HUMAN_FORMAT = "dddd D MMMM YYYY HH:mm ZZZ" diff --git a/silkaj/money/history.py b/silkaj/money/history.py index 93fb1059156539f18da3cd21503c5c3d49516a16..b1de80d7ef063e2299f01799a20f8d7db10fbf69 100644 --- a/silkaj/money/history.py +++ b/silkaj/money/history.py @@ -19,7 +19,7 @@ from pathlib import Path from typing import Any, Optional from urllib.error import HTTPError -import arrow +import pendulum import rich_click as click from duniterpy.api.bma.tx import history from duniterpy.api.client import Client @@ -109,7 +109,7 @@ def generate_header(pubkey: str, currency_symbol: str, ud_value: int) -> str: idty = {"uid": ""} balance = mt.get_amount_from_pubkey(pubkey) balance_ud = round(balance[1] / ud_value, 2) - date = arrow.now().format(ALL) + date = pendulum.now().format(ALL) return f"Transactions history from: {idty['uid']} {gen_pubkey_checksum(pubkey)}\n\ Current balance: {balance[1] / CENT_MULT_TO_UNIT} {currency_symbol}, {balance_ud} UD {currency_symbol} on {date}\n" @@ -202,7 +202,9 @@ def parse_received_tx( identities = wt.identities_from_pubkeys(issuers, uids) for received_tx in received_txs: tx_list = [] - tx_list.append(arrow.get(received_tx.time).to("local").format(ALL_DIGITAL)) + tx_list.append( + pendulum.from_timestamp(received_tx.time, tz="local").format(ALL_DIGITAL) + ) tx_list.append("") for i, issuer in enumerate(received_tx.issuers): tx_list[1] += prefix(None, None, i) + assign_idty_from_pubkey( @@ -243,7 +245,9 @@ def parse_sent_tx( identities = wt.identities_from_pubkeys(pubkeys, uids) for sent_tx in sent_txs: tx_list = [] - tx_list.append(arrow.get(sent_tx.time).to("local").format(ALL_DIGITAL)) + tx_list.append( + pendulum.from_timestamp(sent_tx.time, tz="local").format(ALL_DIGITAL) + ) total_amount, outputs = tx_amount(sent_tx, pubkey, sent_func) if len(outputs) > 1: diff --git a/silkaj/wot/certify.py b/silkaj/wot/certify.py index be07e9939bf50daaeb931aeddfb2e3f61b42d5ab..a2af158984364ae673fac1e18b5642c51b1f4da3 100644 --- a/silkaj/wot/certify.py +++ b/silkaj/wot/certify.py @@ -15,7 +15,7 @@ import sys -import arrow +import pendulum import rich_click as click from duniterpy.api import bma from duniterpy.api.client import Client @@ -103,7 +103,7 @@ def pre_checks(client: Client, issuer_pubkey: str, pubkey_to_certify: str) -> di # ĞT: 0<->4.8m - 4.8m + 12.5d renewable = cert["expiresIn"] - params["sigValidity"] + params["sigReplay"] if renewable > 0: - renewable_date = arrow.now().shift(seconds=renewable).format(DATE) + renewable_date = pendulum.now().add(seconds=renewable).format(DATE) sys.exit(f"Certification renewable from {renewable_date}") # Check if the certification is already in the pending certifications @@ -125,7 +125,7 @@ def certification_confirmation( idty_timestamp = idty_to_certify["meta"]["timestamp"] block_id_idty = get_block_id(idty_timestamp) block = client(bma.blockchain.block, block_id_idty.number) - timestamp_date = arrow.get(block["time"]).to("local").format(ALL) + timestamp_date = pendulum.from_timestamp(block["time"], tz="local").format(ALL) block_id_date = f": #{idty_timestamp[:15]}… {timestamp_date}" cert.append(["ID", issuer["uid"], "->", idty_to_certify["uid"] + block_id_date]) cert.append( @@ -137,8 +137,8 @@ def certification_confirmation( ], ) params = bc_tools.get_blockchain_parameters() - cert_ends = arrow.now().shift(seconds=params["sigValidity"]).format(DATE) - cert.append(["Valid", arrow.now().format(DATE), "—>", cert_ends]) + cert_ends = pendulum.now().add(seconds=params["sigValidity"]).format(DATE) + cert.append(["Valid", pendulum.now().format(DATE), "—>", cert_ends]) table = tui.Table() table.fill_rows( diff --git a/silkaj/wot/exclusions.py b/silkaj/wot/exclusions.py index 1a65ca58e213837acc9f116e65379c1e69ec7dcb..1b700ffa523a1d4e437e06281834fc50f6f10145 100644 --- a/silkaj/wot/exclusions.py +++ b/silkaj/wot/exclusions.py @@ -19,7 +19,7 @@ import sys import time import urllib -import arrow +import pendulum import rich_click as click from duniterpy import constants as dp_const from duniterpy.api.bma import blockchain @@ -222,8 +222,8 @@ def generate_identity_info(lookup, block, params): for i, certified in enumerate(lookup["signed"]): info += elements_inbetween_list(i, lookup["signed"]) info += "@" + certified["uid"] - dt = arrow.get(block.mediantime).shift(hours=1).to(tz="local") - info += ".\n- **Exclu·e le** " + dt.format(constants.FULL_HUMAN_FORMAT, locale="fr") + dt = pendulum.from_timestamp(block.mediantime + constants.ONE_HOUR, tz="local") + info += ".\n- **Exclu·e le** " + dt.format("LLLL zz", locale="fr") info += "\n- **Raison de l'exclusion** : " if nbr_different_certifiers < params["sigQty"]: info += "manque de certifications" diff --git a/silkaj/wot/idty_tools.py b/silkaj/wot/idty_tools.py index 2febdafb6323795e4e7d983b3d67b1fd563285ae..723c28ff8e8e4287f27e5481ab5c9907b99d8ba3 100644 --- a/silkaj/wot/idty_tools.py +++ b/silkaj/wot/idty_tools.py @@ -18,7 +18,7 @@ import sys import urllib from typing import Union -import arrow +import pendulum import rich_click as click from duniterpy.api import bma from duniterpy.documents import BlockID, Identity, Revocation @@ -40,7 +40,9 @@ def display_identity(idty: Identity) -> Texttable: id_table.append(["User ID", idty.uid]) id_table.append(["Blockstamp", str(idty.block_id)]) creation_block = client(bma.blockchain.block, idty.block_id.number) - creation_date = arrow.get(creation_block["time"]).to("local").format(ALL) + creation_date = pendulum.from_timestamp(creation_block["time"], tz="local").format( + ALL, + ) id_table.append(["Created on", creation_date]) # display infos table = Texttable(max_width=shutil.get_terminal_size().columns) diff --git a/silkaj/wot/membership.py b/silkaj/wot/membership.py index 92f7013d7d654df0486a3fd3342dc48b98c0ffa5..c48b0b4e1dd7fd422abbaa896e37401f31fa762a 100644 --- a/silkaj/wot/membership.py +++ b/silkaj/wot/membership.py @@ -15,7 +15,7 @@ import logging -import arrow +import pendulum import rich_click as click from duniterpy.api import bma from duniterpy.documents import BlockID, Membership, get_block_id @@ -104,7 +104,7 @@ def display_confirmation_table( table = [] if membership_expires: - expires = arrow.now().shift(seconds=membership_expires).humanize() + expires = pendulum.now().add(seconds=membership_expires).diff_for_humans() table.append(["Expiration date of current membership", expires]) if pending_memberships: @@ -118,7 +118,7 @@ def display_confirmation_table( table.append( [ "Pending membership documents will expire", - arrow.now().shift(seconds=pending_expires).humanize(), + pendulum.now().add(seconds=pending_expires).diff_for_humans(), ], ) @@ -131,7 +131,7 @@ def display_confirmation_table( table.append( [ "Identity published", - arrow.get(block["time"]).to("local").format(DATE), + pendulum.from_timestamp(block["time"], tz="local").format(DATE), ], ) @@ -139,14 +139,14 @@ def display_confirmation_table( table.append( [ "Expiration date of new membership", - arrow.now().shift(seconds=params["msValidity"]).humanize(), + pendulum.now().add(seconds=params["msValidity"]).diff_for_humans(), ], ) table.append( [ "Expiration date of new membership from the mempool", - arrow.now().shift(seconds=params["msPeriod"]).humanize(), + pendulum.now().add(seconds=params["msPeriod"]).diff_for_humans(), ], ) diff --git a/silkaj/wot/status.py b/silkaj/wot/status.py index e0fb2630883c341a45fd5e9e662800d7205ba7ed..fda49eb8eb6ee629d0f0dd088fe953772b8cf5a8 100644 --- a/silkaj/wot/status.py +++ b/silkaj/wot/status.py @@ -13,7 +13,7 @@ # You should have received a copy of the GNU Affero General Public License # along with Silkaj. If not, see <https://www.gnu.org/licenses/>. -import arrow +import pendulum import rich_click as click from duniterpy.api.bma import blockchain, wot @@ -58,7 +58,7 @@ def status(uid_pubkey: str) -> None: for req_cert in req["certifications"]: if req_cert["from"] == lookup_cert["pubkey"]: certifications["received_expire"].append( - arrow.now().shift(seconds=req_cert["expiresIn"]).format(DATE), + pendulum.now().add(seconds=req_cert["expiresIn"]).format(DATE), ) certifications["received"].append(f"{lookup_cert['uids'][0]} ✔") break @@ -67,7 +67,9 @@ def status(uid_pubkey: str) -> None: f"{(wt.identity_of(pending_cert['from']))['uid']} ✘", ) certifications["received_expire"].append( - arrow.get(pending_cert["expires_on"]).to("local").format(DATE), + pendulum.from_timestamp(pending_cert["expires_on"], tz="local").format( + DATE + ), ) certifications["sent"], certifications["sent_expire"] = get_sent_certifications( signed, @@ -103,13 +105,15 @@ def membership_status(certifications: dict, pubkey: str, req: dict) -> None: is_member = bool(member_lookup) print("member:", is_member) if req["revoked"]: - revoke_date = arrow.get(req["revoked_on"]).to("local").format(DATE) + revoke_date = pendulum.from_timestamp(req["revoked_on"], tz="local").format( + DATE + ) print(f"revoked: {req['revoked']}\nrevoked on: {revoke_date}") if not is_member and req["wasMember"]: print("expired:", req["expired"], "\nwasMember:", req["wasMember"]) elif is_member: expiration_date = ( - arrow.now().shift(seconds=req["membershipExpiresIn"]).format(DATE) + pendulum.now().add(seconds=req["membershipExpiresIn"]).format(DATE) ) print(f"Membership document expiration: {expiration_date}") print("Sentry:", req["isSentry"]) @@ -145,7 +149,7 @@ def expiration_date_from_block_id( date_approximation(block_id, time_first_block, params["avgGenTime"]) + params["sigValidity"] ) - return arrow.get(expir_timestamp).to("local").format(DATE) + return pendulum.from_timestamp(expir_timestamp, tz="local").format(DATE) def date_approximation(block_id, time_first_block, avgentime): diff --git a/tests/unit/money/test_history.py b/tests/unit/money/test_history.py index 78d71a1b29110bdb29f17a19817c5a79b2078a92..5c18edbc316bf1e555ef8f159b19b323971b96d4 100644 --- a/tests/unit/money/test_history.py +++ b/tests/unit/money/test_history.py @@ -16,7 +16,7 @@ import csv from pathlib import Path -import arrow +import pendulum import pytest from click.testing import CliRunner @@ -183,28 +183,28 @@ relative_amount = str(round(CENT_MULT_TO_UNIT / mock_ud_value, 2)) csv_reference = ( ["Date", "Issuers/Recipients", "Amounts Ğ1", "Amounts UDĞ1", "Reference"], [ - arrow.get(111111114).to("local").format(ALL_DIGITAL), + pendulum.from_timestamp(111111114, tz="local").format(ALL_DIGITAL), "CvrMiUhAJpNyX5sdAyZqPE6yEFfSsf6j9EpMmeKvMCWW:DNB", "1.0", relative_amount, "initialisation", ], [ - arrow.get(111111113).to("local").format(ALL_DIGITAL), + pendulum.from_timestamp(111111113, tz="local").format(ALL_DIGITAL), "CmFKubyqbmJWbhyH2eEPVSSs4H4NeXGDfrETzEnRFtPd:CQ5", "1.0", relative_amount, "", ], [ - arrow.get(111111112).to("local").format(ALL_DIGITAL), + pendulum.from_timestamp(111111112, tz="local").format(ALL_DIGITAL), "CvrMiUhAJpNyX5sdAyZqPE6yEFfSsf6j9EpMmeKvMCWW:DNB", "-1.0", f"-{relative_amount}", "", ], [ - arrow.get(111111111).to("local").format(ALL_DIGITAL), + pendulum.from_timestamp(111111111, tz="local").format(ALL_DIGITAL), "CmFKubyqbmJWbhyH2eEPVSSs4H4NeXGDfrETzEnRFtPd:CQ5", "-1.0", f"-{relative_amount}", diff --git a/tests/unit/wot/test_idty_tools.py b/tests/unit/wot/test_idty_tools.py index 4451994b0e8ee54ae12961352e6e5c98897b2b23..3554fba7e082511ec30c8e0a969c238ec3eb63e9 100644 --- a/tests/unit/wot/test_idty_tools.py +++ b/tests/unit/wot/test_idty_tools.py @@ -16,7 +16,7 @@ import re import urllib -import arrow +import pendulum import pytest from duniterpy.api import bma from duniterpy.documents.block_id import BlockID @@ -294,7 +294,7 @@ def test_display_identity(idty, monkeypatch, capsys): assert "| User ID | Claude" in result assert "| Blockstamp | 597334" in result # idty_block returns a block at timestamp 1594980185,. - creation_time = arrow.get(idty_block["time"]).to("local").format(ALL) + creation_time = pendulum.from_timestamp(idty_block["time"], tz="local").format(ALL) assert creation_time in result diff --git a/tests/unit/wot/test_membership.py b/tests/unit/wot/test_membership.py index b55f81d840bbc19eb5a240097cb7315e28e615e1..eac0fd3e267ff1dd38456b9baf4e17de200c3045 100644 --- a/tests/unit/wot/test_membership.py +++ b/tests/unit/wot/test_membership.py @@ -13,7 +13,7 @@ # You should have received a copy of the GNU Affero General Public License # along with Silkaj. If not, see <https://www.gnu.org/licenses/>. -import arrow +import pendulum import pytest from duniterpy.api import bma from duniterpy.documents import Membership, get_block_id @@ -73,7 +73,7 @@ def test_display_confirmation_table(patched_wot_requirements, monkeypatch, capsy table.append( [ "Expiration date of current membership", - arrow.now().shift(seconds=membership_expires).humanize(), + pendulum.now().add(seconds=membership_expires).diff_for_humans(), ], ) @@ -87,7 +87,7 @@ def test_display_confirmation_table(patched_wot_requirements, monkeypatch, capsy table.append( [ "Pending membership documents will expire", - arrow.now().shift(seconds=pending_expires).humanize(), + pendulum.now().add(seconds=pending_expires).diff_for_humans(), ], ) @@ -100,7 +100,7 @@ def test_display_confirmation_table(patched_wot_requirements, monkeypatch, capsy table.append( [ "Identity published", - arrow.get(block["time"]).to("local").format(DATE), + pendulum.from_timestamp(block["time"], tz="local").format(DATE), ], ) @@ -108,14 +108,14 @@ def test_display_confirmation_table(patched_wot_requirements, monkeypatch, capsy table.append( [ "Expiration date of new membership", - arrow.now().shift(seconds=params["msValidity"]).humanize(), + pendulum.now().add(seconds=params["msValidity"]).diff_for_humans(), ], ) table.append( [ "Expiration date of new membership from the mempool", - arrow.now().shift(seconds=params["msPeriod"]).humanize(), + pendulum.now().add(seconds=params["msPeriod"]).diff_for_humans(), ], )