Compare commits

..

7 Commits
0.3.0 ... 0.5.0

Author SHA1 Message Date
Elisiário Couto
798a8f1880 chore(ci): Bump version to 0.5.0 2024-03-29 16:57:33 +00:00
Elisiário Couto
7401ca62d2 feat(notifications): Add support for Telegram notifications. 2024-03-29 16:56:45 +00:00
Elisiário Couto
e46634cf27 chore: Rename docker-compose.yml to compose.yml and remove obsolete 'version' key. 2024-03-28 16:09:54 +00:00
Elisiário Couto
7b48bc080c chore(ci): Bump version to 0.4.0 2024-03-28 15:58:59 +00:00
Elisiário Couto
0cb339366c feat(notifications): Add support for transaction filter and notifications via Discord. 2024-03-28 15:58:16 +00:00
Elisiário Couto
3d36198b06 chore: Update dependencies. 2024-03-28 15:58:16 +00:00
dependabot[bot]
2352ea9e58 chore(deps-dev): Bump black from 24.2.0 to 24.3.0
Bumps [black](https://github.com/psf/black) from 24.2.0 to 24.3.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/24.2.0...24.3.0)

---
updated-dependencies:
- dependency-name: black
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-27 23:15:37 +00:00
19 changed files with 450 additions and 306 deletions

1
.gitignore vendored
View File

@@ -162,3 +162,4 @@ data/
docker-compose.dev.yml docker-compose.dev.yml
nocodb/ nocodb/
sql/ sql/
leggen.db

View File

@@ -1,12 +1,12 @@
repos: repos:
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 24.2.0 rev: 24.3.0
hooks: hooks:
- id: black - id: black
language_version: python3.12 language_version: python3.12
- repo: https://github.com/charliermarsh/ruff-pre-commit - repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: "v0.3.1" rev: "v0.3.4"
hooks: hooks:
- id: ruff - id: ruff
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks

View File

@@ -1,3 +1,28 @@
## 0.5.0 (2024/03/29)
### Features
- **notifications:** Add support for Telegram notifications. ([7401ca62](https://github.com/elisiariocouto/leggen/commit/7401ca62d2ff23c4100ed9d1c8b7450289337553))
### Miscellaneous Tasks
- Rename docker-compose.yml to compose.yml and remove obsolete 'version' key. ([e46634cf](https://github.com/elisiariocouto/leggen/commit/e46634cf27046bfc8d638a0cd4910a4a8a42648a))
## 0.4.0 (2024/03/28)
### Features
- **notifications:** Add support for transaction filter and notifications via Discord. ([0cb33936](https://github.com/elisiariocouto/leggen/commit/0cb339366cc5965223144d2829312d9416d4bc46))
### Miscellaneous Tasks
- **deps-dev:** Bump black from 24.2.0 to 24.3.0 ([2352ea9e](https://github.com/elisiariocouto/leggen/commit/2352ea9e58f14250b819e02fa59879e7ff200764))
- Update dependencies. ([3d36198b](https://github.com/elisiariocouto/leggen/commit/3d36198b06eebc9d7480eb020d1a713e8637b31a))
## 0.3.0 (2024/03/08) ## 0.3.0 (2024/03/08)
### Documentation ### Documentation

View File

@@ -26,27 +26,58 @@ Having your bank data in a database, gives you the power to backup, analyze and
- List all connected banks and their statuses - List all connected banks and their statuses
- List balances of all connected accounts - List balances of all connected accounts
- List transactions for all connected accounts - List transactions for all connected accounts
- Sync all transactions with a SQLite or MongoDB database - Sync all transactions with a SQLite and/or MongoDB database
- Visualize and query transactions using NocoDB - Visualize and query transactions using NocoDB
- Schedule regular syncs with the database using Ofelia - Schedule regular syncs with the database using Ofelia
- Send notifications to Discord and/or Telegram when transactions match certain filters
## 🚀 Installation and Configuration ## 🚀 Installation and Configuration
In order to use `leggen`, you need to create a GoCardless account. GoCardless is a service that provides access to Open Banking APIs. You can create an account at https://gocardless.com/bank-account-data/. In order to use `leggen`, you need to create a GoCardless account. GoCardless is a service that provides access to Open Banking APIs. You can create an account at https://gocardless.com/bank-account-data/.
After creating an account and getting your API keys, the best way is to use the [compose file](docker-compose.yml). Open the file and adapt it to your needs. Then run the following command: After creating an account and getting your API keys, the best way is to use the [compose file](compose.yml). Open the file and adapt it to your needs.
### Example Configuration
Create a configuration file at with the following content:
```toml
[gocardless]
key = "your-api-key"
secret = "your-secret-key"
url = "https://bankaccountdata.gocardless.com/api/v2"
[database]
sqlite = true
mongodb = true
[database.mongodb]
uri = "mongodb://localhost:27017"
[notifications.discord]
webhook = "https://discord.com/api/webhooks/..."
[notifications.telegram]
# See gist for telegram instructions
# https://gist.github.com/nafiesl/4ad622f344cd1dc3bb1ecbe468ff9f8a
token = "12345:abcdefghijklmnopqrstuvxwyz"
chat-id = 12345
[filters.case-insensitive]
filter1 = "company-name"
```
### Running Leggen with Docker
After adapting the compose file, run the following command:
```bash ```bash
$ docker compose up -d $ docker compose up -d
``` ```
The leggen container will exit, this is expected. Now you can run the following command to create the configuration file: The leggen container will exit, this is expected since you didn't connect any bank accounts yet.
```bash Run the following command and follow the instructions:
$ docker compose run leggen init
```
Now you need to connect your bank accounts. Run the following command and follow the instructions:
```bash ```bash
$ docker compose run leggen bank add $ docker compose run leggen bank add
@@ -67,15 +98,17 @@ Usage: leggen [OPTIONS] COMMAND [ARGS]...
Leggen: An Open Banking CLI Leggen: An Open Banking CLI
Options: Options:
--version Show the version and exit. --version Show the version and exit.
-h, --help Show this message and exit. -c, --config FILE Path to TOML configuration file
[env var: LEGGEN_CONFIG_FILE;
default: ~/.config/leggen/config.toml]
-h, --help Show this message and exit.
Command Groups: Command Groups:
bank Manage banks connections bank Manage banks connections
Commands: Commands:
balances List balances of all connected accounts balances List balances of all connected accounts
init Create configuration file
status List all connected banks and their status status List all connected banks and their status
sync Sync all transactions with database sync Sync all transactions with database
transactions List transactions transactions List transactions

View File

@@ -1,18 +1,11 @@
version: '3.1'
services: services:
# Defaults to `sync` command. # Defaults to `sync` command.
leggen: leggen:
image: elisiariocouto/leggen:latest image: elisiariocouto/leggen:latest
command: sync command: sync
restart: "no" restart: "no"
environment:
LEGGEN_GC_API_KEY: "changeme"
LEGGEN_GC_API_SECRET: "changeme"
# Uncomment the following lines if you use MongoDB
# LEGGEN_MONGO_URI: "mongodb://leggen:changeme@mongo:27017/leggen"
volumes: volumes:
- "./leggen:/root/.config/leggen" - "./leggen:/root/.config/leggen" # Default configuration file should be in this directory, named `config.toml`
- "./db:/app" - "./db:/app"
nocodb: nocodb:

View File

@@ -1,72 +0,0 @@
import click
from leggen.main import cli
from leggen.utils.auth import get_token
from leggen.utils.config import save_config
@cli.command()
@click.option(
"--api-key",
prompt=True,
help="GoCardless API Key",
envvar="LEGGEN_GC_API_KEY",
show_envvar=True,
)
@click.option(
"--api-secret",
prompt=True,
help="GoCardless API Secret",
hide_input=True,
envvar="LEGGEN_GC_API_SECRET",
show_envvar=True,
)
@click.option(
"--api-url",
default="https://bankaccountdata.gocardless.com/api/v2",
help="GoCardless API URL",
show_default=True,
envvar="LEGGEN_GC_API_URL",
show_envvar=True,
)
@click.option(
"--sqlite/--mongo",
prompt=True,
default=True,
help="Use SQLite or MongoDB",
show_default=True,
)
@click.option(
"--mongo-uri",
prompt=True,
help="MongoDB URI",
envvar="LEGGEN_MONGO_URI",
show_envvar=True,
default="mongodb://localhost:27017",
)
@click.pass_context
def init(
ctx: click.Context,
api_key: str,
api_secret: str,
api_url: str,
sqlite: bool,
mongo_uri: str,
):
"""
Create configuration file
"""
config = {
"api_key": api_key,
"api_secret": api_secret,
"api_url": api_url,
"sqlite": sqlite,
"mongo_uri": mongo_uri,
}
# Just make sure this API credentials are valid
# if so, it will save the token in the auth file
_ = get_token(config)
# Save the configuration
save_config(config)

View File

@@ -1,107 +1,12 @@
from datetime import datetime
import click import click
from leggen.main import cli from leggen.main import cli
from leggen.utils.mongo import save_transactions as save_transactions_mongo from leggen.utils.database import save_transactions
from leggen.utils.network import get from leggen.utils.network import get
from leggen.utils.sqlite import save_transactions as save_transactions_sqlite from leggen.utils.notifications import send_notification
from leggen.utils.text import error, info from leggen.utils.text import error, info
def save_transactions(ctx: click.Context, account: str):
info(f"[{account}] Getting account details")
account_info = get(ctx, f"/accounts/{account}")
info(f"[{account}] Getting transactions")
transactions = []
account_transactions = get(ctx, f"/accounts/{account}/transactions/").get(
"transactions", []
)
for transaction in account_transactions.get("booked", []):
booked_date = transaction.get("bookingDateTime") or transaction.get(
"bookingDate"
)
value_date = transaction.get("valueDateTime") or transaction.get("valueDate")
if booked_date and value_date:
min_date = min(
datetime.fromisoformat(booked_date), datetime.fromisoformat(value_date)
)
else:
min_date = datetime.fromisoformat(booked_date or value_date)
transactionValue = float(
transaction.get("transactionAmount", {}).get("amount", 0)
)
currency = transaction.get("transactionAmount", {}).get("currency", "")
description = transaction.get(
"remittanceInformationUnstructured",
",".join(transaction.get("remittanceInformationUnstructuredArray", [])),
)
t = {
"internalTransactionId": transaction.get("internalTransactionId"),
"institutionId": account_info["institution_id"],
"iban": account_info.get("iban", "N/A"),
"transactionDate": min_date,
"description": description,
"transactionValue": transactionValue,
"transactionCurrency": currency,
"transactionStatus": "booked",
"accountId": account,
"rawTransaction": transaction,
}
transactions.append(t)
for transaction in account_transactions.get("pending", []):
booked_date = transaction.get("bookingDateTime") or transaction.get(
"bookingDate"
)
value_date = transaction.get("valueDateTime") or transaction.get("valueDate")
if booked_date and value_date:
min_date = min(
datetime.fromisoformat(booked_date), datetime.fromisoformat(value_date)
)
else:
min_date = datetime.fromisoformat(booked_date or value_date)
transactionValue = float(
transaction.get("transactionAmount", {}).get("amount", 0)
)
currency = transaction.get("transactionAmount", {}).get("currency", "")
description = transaction.get(
"remittanceInformationUnstructured",
",".join(transaction.get("remittanceInformationUnstructuredArray", [])),
)
t = {
"internalTransactionId": transaction.get("internalTransactionId"),
"institutionId": account_info["institution_id"],
"iban": account_info.get("iban", "N/A"),
"transactionDate": min_date,
"description": description,
"transactionValue": transactionValue,
"transactionCurrency": currency,
"transactionStatus": "pending",
"accountId": account,
"rawTransaction": transaction,
}
transactions.append(t)
sqlite = ctx.obj["sqlite"]
info(
f"[{account}] Fetched {len(transactions)} transactions, saving to {'SQLite' if sqlite else 'MongoDB'}"
)
if sqlite:
save_transactions_sqlite(ctx, account, transactions)
else:
save_transactions_mongo(ctx, account, transactions)
@cli.command() @cli.command()
@click.pass_context @click.pass_context
def sync(ctx: click.Context): def sync(ctx: click.Context):
@@ -118,6 +23,12 @@ def sync(ctx: click.Context):
for account in accounts: for account in accounts:
try: try:
save_transactions(ctx, account) new_transactions = save_transactions(ctx, account)
except Exception: except Exception as e:
error(f"[{account}] Error: Sync failed, skipping account.") error(f"[{account}] Error: Sync failed, skipping account, exception: {e}")
continue
try:
send_notification(ctx, new_transactions)
except Exception as e:
error(f"[{account}] Error: Notification failed, exception: {e}")
continue

View File

@@ -5,9 +5,9 @@ from pymongo.errors import DuplicateKeyError
from leggen.utils.text import success, warning from leggen.utils.text import success, warning
def save_transactions(ctx: click.Context, account: str, transactions: list): def persist_transactions(ctx: click.Context, account: str, transactions: list) -> list:
# Connect to MongoDB # Connect to MongoDB
mongo_uri = ctx.obj["mongo_uri"] mongo_uri = ctx.obj.get("database", {}).get("mongodb", {}).get("uri")
client = MongoClient(mongo_uri) client = MongoClient(mongo_uri)
db = client["leggen"] db = client["leggen"]
transactions_collection = db["transactions"] transactions_collection = db["transactions"]
@@ -16,17 +16,20 @@ def save_transactions(ctx: click.Context, account: str, transactions: list):
transactions_collection.create_index("internalTransactionId", unique=True) transactions_collection.create_index("internalTransactionId", unique=True)
# Insert transactions into MongoDB # Insert transactions into MongoDB
new_transactions_count = 0
duplicates_count = 0 duplicates_count = 0
new_transactions = []
for transaction in transactions: for transaction in transactions:
try: try:
transactions_collection.insert_one(transaction) transactions_collection.insert_one(transaction)
new_transactions_count += 1 new_transactions.append(transaction)
except DuplicateKeyError: except DuplicateKeyError:
# A transaction with the same ID already exists, skip insertion # A transaction with the same ID already exists, skip insertion
duplicates_count += 1 duplicates_count += 1
success(f"[{account}] Inserted {new_transactions_count} new transactions") success(f"[{account}] Inserted {len(new_transactions)} new transactions")
if duplicates_count: if duplicates_count:
warning(f"[{account}] Skipped {duplicates_count} duplicate transactions") warning(f"[{account}] Skipped {duplicates_count} duplicate transactions")
return new_transactions

View File

@@ -7,7 +7,7 @@ import click
from leggen.utils.text import success, warning from leggen.utils.text import success, warning
def save_transactions(ctx: click.Context, account: str, transactions: list): def persist_transactions(ctx: click.Context, account: str, transactions: list) -> list:
# Path to your SQLite database file # Path to your SQLite database file
# Connect to SQLite database # Connect to SQLite database
@@ -31,7 +31,6 @@ def save_transactions(ctx: click.Context, account: str, transactions: list):
) )
# Insert transactions into SQLite database # Insert transactions into SQLite database
new_transactions_count = 0
duplicates_count = 0 duplicates_count = 0
# Prepare an SQL statement for inserting data # Prepare an SQL statement for inserting data
@@ -48,6 +47,8 @@ def save_transactions(ctx: click.Context, account: str, transactions: list):
rawTransaction rawTransaction
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""" ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"""
new_transactions = []
for transaction in transactions: for transaction in transactions:
try: try:
cursor.execute( cursor.execute(
@@ -65,8 +66,7 @@ def save_transactions(ctx: click.Context, account: str, transactions: list):
json.dumps(transaction["rawTransaction"]), json.dumps(transaction["rawTransaction"]),
), ),
) )
new_transactions.append(transaction)
new_transactions_count += 1
except IntegrityError: except IntegrityError:
# A transaction with the same ID already exists, indicating a duplicate # A transaction with the same ID already exists, indicating a duplicate
duplicates_count += 1 duplicates_count += 1
@@ -75,6 +75,8 @@ def save_transactions(ctx: click.Context, account: str, transactions: list):
conn.commit() conn.commit()
conn.close() conn.close()
success(f"[{account}] Inserted {new_transactions_count} new transactions") success(f"[{account}] Inserted {len(new_transactions)} new transactions")
if duplicates_count: if duplicates_count:
warning(f"[{account}] Skipped {duplicates_count} duplicate transactions") warning(f"[{account}] Skipped {duplicates_count} duplicate transactions")
return new_transactions

View File

@@ -74,30 +74,33 @@ class Group(click.Group):
return getattr(mod, name) return getattr(mod, name)
@click.group(cls=Group, context_settings={"help_option_names": ["-h", "--help"]}) @click.option(
"-c",
"--config",
type=click.Path(dir_okay=False),
default=click.get_app_dir("leggen") / Path("config.toml"),
show_default=True,
callback=load_config,
is_eager=True,
expose_value=False,
envvar="LEGGEN_CONFIG_FILE",
show_envvar=True,
help="Path to TOML configuration file",
)
@click.group(
cls=Group,
context_settings={"help_option_names": ["-h", "--help"]},
)
@click.version_option(package_name="leggen") @click.version_option(package_name="leggen")
@click.pass_context @click.pass_context
def cli(ctx: click.Context): def cli(ctx: click.Context):
""" """
Leggen: An Open Banking CLI Leggen: An Open Banking CLI
""" """
ctx.ensure_object(dict)
# Do not require authentication when printing help messages # Do not require authentication when printing help messages
if "--help" in sys.argv[1:] or "-h" in sys.argv[1:]: if "--help" in sys.argv[1:] or "-h" in sys.argv[1:]:
return return
# or when running the init command token = get_token(ctx)
if ctx.invoked_subcommand == "init":
if (click.get_app_dir("leggen") / Path("config.json")).is_file():
click.confirm(
"Configuration file already exists. Do you want to overwrite it?",
abort=True,
)
return
config = load_config()
token = get_token(config)
ctx.obj["api_url"] = config["api_url"]
ctx.obj["sqlite"] = config["sqlite"]
ctx.obj["mongo_uri"] = config["mongo_uri"]
ctx.obj["headers"] = {"Authorization": f"Bearer {token}"} ctx.obj["headers"] = {"Authorization": f"Bearer {token}"}

View File

@@ -0,0 +1,33 @@
import click
from discord_webhook import DiscordEmbed, DiscordWebhook
from leggen.utils.text import info
def send_message(ctx: click.Context, transactions: list):
info(f"Got {len(transactions)} new transactions, sending message to Discord")
webhook = DiscordWebhook(url=ctx.obj["notifications"]["discord"]["webhook"])
embed = DiscordEmbed(
title="",
description=f"{len(transactions)} new transaction matches",
color="03b2f8",
)
embed.set_author(
name="Leggen",
url="https://github.com/elisiariocouto/leggen",
)
embed.set_footer(text="Case-insensitive filters")
embed.set_timestamp()
for transaction in transactions:
embed.add_embed_field(
name=transaction["name"],
value=f"{transaction['value']}{transaction['currency']} ({transaction['date']})",
)
webhook.add_embed(embed)
response = webhook.execute()
try:
response.raise_for_status()
except Exception as e:
raise Exception(f"Discord notification failed: {e}\n{response.text}") from e

View File

@@ -0,0 +1,43 @@
import click
import requests
from leggen.utils.text import info
def escape_markdown(text: str) -> str:
return (
str(text)
.replace("-", "\\-")
.replace("#", "\\#")
.replace(".", "\\.")
.replace("$", "\\$")
.replace("+", "\\+")
)
def send_message(ctx: click.Context, transactions: list):
token = ctx.obj["notifications"]["telegram"]["api-key"]
chat_id = ctx.obj["notifications"]["telegram"]["chat-id"]
bot_url = f"https://api.telegram.org/bot{token}/sendMessage"
info(f"Got {len(transactions)} new transactions, sending message to Telegram")
message = "*💲 [Leggen](https://github.com/elisiariocouto/leggen)*\n"
message += f"{len(transactions)} new transaction matches\n\n"
for transaction in transactions:
message += f"*Name*: {transaction['name']}\n"
message += f"*Value*: {transaction['value']}{transaction['currency']}\n"
message += f"*Date*: {transaction['date']}\n\n"
res = requests.post(
bot_url,
json={
"chat_id": chat_id,
"text": escape_markdown(message),
"parse_mode": "MarkdownV2",
},
)
try:
res.raise_for_status()
except Exception as e:
raise Exception(f"Telegram notification failed: {e}\n{res.text}") from e

View File

@@ -7,13 +7,16 @@ import requests
from leggen.utils.text import warning from leggen.utils.text import warning
def create_token(config: dict) -> str: def create_token(ctx: click.Context) -> str:
""" """
Create a new token Create a new token
""" """
res = requests.post( res = requests.post(
f"{config['api_url']}/token/new/", f"{ctx.obj['gocardless']['url']}/token/new/",
json={"secret_id": config["api_key"], "secret_key": config["api_secret"]}, json={
"secret_id": ctx.obj["gocardless"]["key"],
"secret_key": ctx.obj["gocardless"]["secret"],
},
) )
res.raise_for_status() res.raise_for_status()
auth = res.json() auth = res.json()
@@ -21,7 +24,7 @@ def create_token(config: dict) -> str:
return auth["access"] return auth["access"]
def get_token(config: dict) -> str: def get_token(ctx: click.Context) -> str:
""" """
Get the token from the auth file or request a new one Get the token from the auth file or request a new one
""" """
@@ -30,10 +33,11 @@ def get_token(config: dict) -> str:
with click.open_file(str(auth_file), "r") as f: with click.open_file(str(auth_file), "r") as f:
auth = json.load(f) auth = json.load(f)
if not auth.get("access"): if not auth.get("access"):
return create_token(config) return create_token(ctx)
res = requests.post( res = requests.post(
f"{config['api_url']}/token/refresh/", json={"refresh": auth["refresh"]} f"{ctx.obj['gocardless']['url']}/token/refresh/",
json={"refresh": auth["refresh"]},
) )
try: try:
res.raise_for_status() res.raise_for_status()
@@ -44,9 +48,9 @@ def get_token(config: dict) -> str:
warning( warning(
f"Token probably expired, requesting a new one.\nResponse: {res.status_code}\n{res.text}" f"Token probably expired, requesting a new one.\nResponse: {res.status_code}\n{res.text}"
) )
return create_token(config) return create_token(ctx)
else: else:
return create_token(config) return create_token(ctx)
def save_auth(d: dict): def save_auth(d: dict):

View File

@@ -1,29 +1,18 @@
import json
import sys import sys
from pathlib import Path
import click import click
import tomllib
from leggen.utils.text import error, info from leggen.utils.text import error
def save_config(d: dict): def load_config(ctx: click.Context, _, filename):
Path.mkdir(Path(click.get_app_dir("leggen")), exist_ok=True)
config_file = click.get_app_dir("leggen") / Path("config.json")
with click.open_file(str(config_file), "w") as f:
json.dump(d, f)
info(f"Wrote configuration file at '{config_file}'")
def load_config() -> dict:
config_file = click.get_app_dir("leggen") / Path("config.json")
try: try:
with click.open_file(str(config_file), "r") as f: with click.open_file(str(filename), "rb") as f:
config = json.load(f) # TODO: Implement configuration file validation (use pydantic?)
return config ctx.obj = tomllib.load(f)
except FileNotFoundError: except FileNotFoundError:
error( error(
"Configuration file not found. Run `leggen init` to configure your account." "Configuration file not found. Provide a valid configuration file path with leggen --config <path> or LEGGEN_CONFIG=<path> environment variable."
) )
sys.exit(1) sys.exit(1)

112
leggen/utils/database.py Normal file
View File

@@ -0,0 +1,112 @@
from datetime import datetime
import click
import leggen.database.mongo as mongodb_engine
import leggen.database.sqlite as sqlite_engine
from leggen.utils.network import get
from leggen.utils.text import info, warning
def persist_transactions(ctx: click.Context, account: str, transactions: list) -> list:
sqlite = ctx.obj.get("database", {}).get("sqlite", False)
mongodb = ctx.obj.get("database", {}).get("mongodb", False)
if not sqlite and not mongodb:
warning("No database engine is enabled, skipping transaction saving")
# WARNING: This will return the transactions list as is, without saving it to any database
# Possible duplicate notifications will be sent if the filters are enabled
return transactions
if sqlite:
info(f"[{account}] Fetched {len(transactions)} transactions, saving to SQLite")
return sqlite_engine.persist_transactions(ctx, account, transactions)
else:
info(f"[{account}] Fetched {len(transactions)} transactions, saving to MongoDB")
return mongodb_engine.persist_transactions(ctx, account, transactions)
def save_transactions(ctx: click.Context, account: str) -> list:
info(f"[{account}] Getting account details")
account_info = get(ctx, f"/accounts/{account}")
info(f"[{account}] Getting transactions")
transactions = []
account_transactions = get(ctx, f"/accounts/{account}/transactions/").get(
"transactions", []
)
for transaction in account_transactions.get("booked", []):
booked_date = transaction.get("bookingDateTime") or transaction.get(
"bookingDate"
)
value_date = transaction.get("valueDateTime") or transaction.get("valueDate")
if booked_date and value_date:
min_date = min(
datetime.fromisoformat(booked_date), datetime.fromisoformat(value_date)
)
else:
min_date = datetime.fromisoformat(booked_date or value_date)
transactionValue = float(
transaction.get("transactionAmount", {}).get("amount", 0)
)
currency = transaction.get("transactionAmount", {}).get("currency", "")
description = transaction.get(
"remittanceInformationUnstructured",
",".join(transaction.get("remittanceInformationUnstructuredArray", [])),
)
t = {
"internalTransactionId": transaction.get("internalTransactionId"),
"institutionId": account_info["institution_id"],
"iban": account_info.get("iban", "N/A"),
"transactionDate": min_date,
"description": description,
"transactionValue": transactionValue,
"transactionCurrency": currency,
"transactionStatus": "booked",
"accountId": account,
"rawTransaction": transaction,
}
transactions.append(t)
for transaction in account_transactions.get("pending", []):
booked_date = transaction.get("bookingDateTime") or transaction.get(
"bookingDate"
)
value_date = transaction.get("valueDateTime") or transaction.get("valueDate")
if booked_date and value_date:
min_date = min(
datetime.fromisoformat(booked_date), datetime.fromisoformat(value_date)
)
else:
min_date = datetime.fromisoformat(booked_date or value_date)
transactionValue = float(
transaction.get("transactionAmount", {}).get("amount", 0)
)
currency = transaction.get("transactionAmount", {}).get("currency", "")
description = transaction.get(
"remittanceInformationUnstructured",
",".join(transaction.get("remittanceInformationUnstructuredArray", [])),
)
t = {
"internalTransactionId": transaction.get("internalTransactionId"),
"institutionId": account_info["institution_id"],
"iban": account_info.get("iban", "N/A"),
"transactionDate": min_date,
"description": description,
"transactionValue": transactionValue,
"transactionCurrency": currency,
"transactionStatus": "pending",
"accountId": account,
"rawTransaction": transaction,
}
transactions.append(t)
return persist_transactions(ctx, account, transactions)

View File

@@ -9,7 +9,7 @@ def get(ctx: click.Context, path: str, params: dict = {}):
GET request to the GoCardless API GET request to the GoCardless API
""" """
url = f"{ctx.obj['api_url']}{path}" url = f"{ctx.obj['gocardless']['url']}{path}"
res = requests.get(url, headers=ctx.obj["headers"], params=params) res = requests.get(url, headers=ctx.obj["headers"], params=params)
try: try:
res.raise_for_status() res.raise_for_status()
@@ -24,7 +24,7 @@ def post(ctx: click.Context, path: str, data: dict = {}):
POST request to the GoCardless API POST request to the GoCardless API
""" """
url = f"{ctx.obj['api_url']}{path}" url = f"{ctx.obj['gocardless']['url']}{path}"
res = requests.post(url, headers=ctx.obj["headers"], json=data) res = requests.post(url, headers=ctx.obj["headers"], json=data)
try: try:
res.raise_for_status() res.raise_for_status()
@@ -39,7 +39,7 @@ def put(ctx: click.Context, path: str, data: dict = {}):
PUT request to the GoCardless API PUT request to the GoCardless API
""" """
url = f"{ctx.obj['api_url']}{path}" url = f"{ctx.obj['gocardless']['url']}{path}"
res = requests.put(url, headers=ctx.obj["headers"], json=data) res = requests.put(url, headers=ctx.obj["headers"], json=data)
try: try:
res.raise_for_status() res.raise_for_status()
@@ -54,7 +54,7 @@ def delete(ctx: click.Context, path: str):
DELETE request to the GoCardless API DELETE request to the GoCardless API
""" """
url = f"{ctx.obj['api_url']}{path}" url = f"{ctx.obj['gocardless']['url']}{path}"
res = requests.delete(url, headers=ctx.obj["headers"]) res = requests.delete(url, headers=ctx.obj["headers"])
try: try:
res.raise_for_status() res.raise_for_status()

View File

@@ -0,0 +1,46 @@
import click
import leggen.notifications.discord as discord
import leggen.notifications.telegram as telegram
from leggen.utils.text import info, warning
def send_notification(ctx: click.Context, transactions: list):
if ctx.obj.get("filters") is None:
warning("No filters are enabled, skipping notifications")
return
filters_case_insensitive = ctx.obj.get("filters", {}).get("case-insensitive", {})
# Add transaction to the list of transactions to be sent as a notification
notification_transactions = []
for transaction in transactions:
for _, v in filters_case_insensitive.items():
if v.lower() in transaction["description"].lower():
notification_transactions.append(
{
"name": transaction["description"],
"value": transaction["transactionValue"],
"currency": transaction["transactionCurrency"],
"date": transaction["transactionDate"],
}
)
if len(notification_transactions) == 0:
warning("No transactions matched the filters, skipping notifications")
return
discord_enabled = ctx.obj.get("notifications", {}).get("discord", False)
telegram_enabled = ctx.obj.get("notifications", {}).get("telegram", False)
if not discord_enabled and not telegram_enabled:
warning("No notification engine is enabled, skipping notifications")
return
if discord_enabled:
info(f"Sending {len(notification_transactions)} transactions to Discord")
discord.send_message(ctx, notification_transactions)
if telegram_enabled:
info(f"Sending {len(notification_transactions)} transactions to Telegram")
telegram.send_message(ctx, notification_transactions)

131
poetry.lock generated
View File

@@ -2,33 +2,33 @@
[[package]] [[package]]
name = "black" name = "black"
version = "24.2.0" version = "24.3.0"
description = "The uncompromising code formatter." description = "The uncompromising code formatter."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "black-24.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6981eae48b3b33399c8757036c7f5d48a535b962a7c2310d19361edeef64ce29"}, {file = "black-24.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395"},
{file = "black-24.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d533d5e3259720fdbc1b37444491b024003e012c5173f7d06825a77508085430"}, {file = "black-24.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995"},
{file = "black-24.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61a0391772490ddfb8a693c067df1ef5227257e72b0e4108482b8d41b5aee13f"}, {file = "black-24.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7"},
{file = "black-24.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:992e451b04667116680cb88f63449267c13e1ad134f30087dec8527242e9862a"}, {file = "black-24.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0"},
{file = "black-24.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd"}, {file = "black-24.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9"},
{file = "black-24.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2"}, {file = "black-24.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597"},
{file = "black-24.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92"}, {file = "black-24.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d"},
{file = "black-24.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23"}, {file = "black-24.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5"},
{file = "black-24.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b"}, {file = "black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f"},
{file = "black-24.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9"}, {file = "black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11"},
{file = "black-24.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693"}, {file = "black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4"},
{file = "black-24.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982"}, {file = "black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5"},
{file = "black-24.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e53a8c630f71db01b28cd9602a1ada68c937cbf2c333e6ed041390d6968faf4"}, {file = "black-24.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837"},
{file = "black-24.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:93601c2deb321b4bad8f95df408e3fb3943d85012dddb6121336b8e24a0d1218"}, {file = "black-24.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd"},
{file = "black-24.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0057f800de6acc4407fe75bb147b0c2b5cbb7c3ed110d3e5999cd01184d53b0"}, {file = "black-24.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213"},
{file = "black-24.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:faf2ee02e6612577ba0181f4347bcbcf591eb122f7841ae5ba233d12c39dcb4d"}, {file = "black-24.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959"},
{file = "black-24.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8"}, {file = "black-24.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb"},
{file = "black-24.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8"}, {file = "black-24.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7"},
{file = "black-24.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540"}, {file = "black-24.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7"},
{file = "black-24.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31"}, {file = "black-24.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f"},
{file = "black-24.2.0-py3-none-any.whl", hash = "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6"}, {file = "black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93"},
{file = "black-24.2.0.tar.gz", hash = "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894"}, {file = "black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f"},
] ]
[package.dependencies] [package.dependencies]
@@ -190,6 +190,23 @@ files = [
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
] ]
[[package]]
name = "discord-webhook"
version = "1.3.1"
description = "Easily send Discord webhooks with Python"
optional = false
python-versions = ">=3.10,<4.0"
files = [
{file = "discord_webhook-1.3.1-py3-none-any.whl", hash = "sha256:ede07028316de76d24eb811836e2b818b2017510da786777adcb0d5970e7af79"},
{file = "discord_webhook-1.3.1.tar.gz", hash = "sha256:ee3e0f3ea4f3dc8dc42be91f75b894a01624c6c13fea28e23ebcf9a6c9a304f7"},
]
[package.dependencies]
requests = ">=2.28.1,<3.0.0"
[package.extras]
async = ["httpx (>=0.23.0,<0.24.0)"]
[[package]] [[package]]
name = "distlib" name = "distlib"
version = "0.3.8" version = "0.3.8"
@@ -223,18 +240,18 @@ wmi = ["wmi (>=1.5.1)"]
[[package]] [[package]]
name = "filelock" name = "filelock"
version = "3.13.1" version = "3.13.3"
description = "A platform independent file lock." description = "A platform independent file lock."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, {file = "filelock-3.13.3-py3-none-any.whl", hash = "sha256:5ffa845303983e7a0b7ae17636509bc97997d58afeafa72fb141a17b152284cb"},
{file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, {file = "filelock-3.13.3.tar.gz", hash = "sha256:a79895a25bbefdf55d1a2a0a80968f7dbb28edcd6d4234a0afb3f37ecde4b546"},
] ]
[package.extras] [package.extras]
docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"]
typing = ["typing-extensions (>=4.8)"] typing = ["typing-extensions (>=4.8)"]
[[package]] [[package]]
@@ -307,13 +324,13 @@ setuptools = "*"
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "23.2" version = "24.0"
description = "Core utilities for Python packages" description = "Core utilities for Python packages"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"},
{file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"},
] ]
[[package]] [[package]]
@@ -344,13 +361,13 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-
[[package]] [[package]]
name = "pre-commit" name = "pre-commit"
version = "3.6.2" version = "3.7.0"
description = "A framework for managing and maintaining multi-language pre-commit hooks." description = "A framework for managing and maintaining multi-language pre-commit hooks."
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
files = [ files = [
{file = "pre_commit-3.6.2-py2.py3-none-any.whl", hash = "sha256:ba637c2d7a670c10daedc059f5c49b5bd0aadbccfcd7ec15592cf9665117532c"}, {file = "pre_commit-3.7.0-py2.py3-none-any.whl", hash = "sha256:5eae9e10c2b5ac51577c3452ec0a490455c45a0533f7960f993a0d01e59decab"},
{file = "pre_commit-3.6.2.tar.gz", hash = "sha256:c3ef34f463045c88658c5b99f38c1e297abdcc0ff13f98d3370055fbbfabc67e"}, {file = "pre_commit-3.7.0.tar.gz", hash = "sha256:e209d61b8acdcf742404408531f0c37d49d2c734fd7cff2d6076083d191cb060"},
] ]
[package.dependencies] [package.dependencies]
@@ -535,44 +552,44 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.3.1" version = "0.3.4"
description = "An extremely fast Python linter and code formatter, written in Rust." description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "ruff-0.3.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6b82e3937d0d76554cd5796bc3342a7d40de44494d29ff490022d7a52c501744"}, {file = "ruff-0.3.4-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:60c870a7d46efcbc8385d27ec07fe534ac32f3b251e4fc44b3cbfd9e09609ef4"},
{file = "ruff-0.3.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ae7954c8f692b70e6a206087ae3988acc9295d84c550f8d90b66c62424c16771"}, {file = "ruff-0.3.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6fc14fa742e1d8f24910e1fff0bd5e26d395b0e0e04cc1b15c7c5e5fe5b4af91"},
{file = "ruff-0.3.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b730f56ccf91225da0f06cfe421e83b8cc27b2a79393db9c3df02ed7e2bbc01"}, {file = "ruff-0.3.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3ee7880f653cc03749a3bfea720cf2a192e4f884925b0cf7eecce82f0ce5854"},
{file = "ruff-0.3.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c78bfa85637668f47bd82aa2ae17de2b34221ac23fea30926f6409f9e37fc927"}, {file = "ruff-0.3.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf133dd744f2470b347f602452a88e70dadfbe0fcfb5fd46e093d55da65f82f7"},
{file = "ruff-0.3.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6abaad602d6e6daaec444cbf4d9364df0a783e49604c21499f75bb92237d4af"}, {file = "ruff-0.3.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f3860057590e810c7ffea75669bdc6927bfd91e29b4baa9258fd48b540a4365"},
{file = "ruff-0.3.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5f0c21b6914c3c9a25a59497cbb1e5b6c2d8d9beecc9b8e03ee986e24eee072e"}, {file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:986f2377f7cf12efac1f515fc1a5b753c000ed1e0a6de96747cdf2da20a1b369"},
{file = "ruff-0.3.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:434c3fc72e6311c85cd143c4c448b0e60e025a9ac1781e63ba222579a8c29200"}, {file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fd98e85869603e65f554fdc5cddf0712e352fe6e61d29d5a6fe087ec82b76c"},
{file = "ruff-0.3.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78a7025e6312cbba496341da5062e7cdd47d95f45c1b903e635cdeb1ba5ec2b9"}, {file = "ruff-0.3.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64abeed785dad51801b423fa51840b1764b35d6c461ea8caef9cf9e5e5ab34d9"},
{file = "ruff-0.3.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52b02bb46f1a79b0c1fa93f6495bc7e77e4ef76e6c28995b4974a20ed09c0833"}, {file = "ruff-0.3.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df52972138318bc7546d92348a1ee58449bc3f9eaf0db278906eb511889c4b50"},
{file = "ruff-0.3.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:11b5699c42f7d0b771c633d620f2cb22e727fb226273aba775a91784a9ed856c"}, {file = "ruff-0.3.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:98e98300056445ba2cc27d0b325fd044dc17fcc38e4e4d2c7711585bd0a958ed"},
{file = "ruff-0.3.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:54e5dca3e411772b51194b3102b5f23b36961e8ede463776b289b78180df71a0"}, {file = "ruff-0.3.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:519cf6a0ebed244dce1dc8aecd3dc99add7a2ee15bb68cf19588bb5bf58e0488"},
{file = "ruff-0.3.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:951efb610c5844e668bbec4f71cf704f8645cf3106e13f283413969527ebfded"}, {file = "ruff-0.3.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bb0acfb921030d00070539c038cd24bb1df73a2981e9f55942514af8b17be94e"},
{file = "ruff-0.3.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:09c7333b25e983aabcf6e38445252cff0b4745420fc3bda45b8fce791cc7e9ce"}, {file = "ruff-0.3.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cf187a7e7098233d0d0c71175375c5162f880126c4c716fa28a8ac418dcf3378"},
{file = "ruff-0.3.1-py3-none-win32.whl", hash = "sha256:d937f9b99ebf346e0606c3faf43c1e297a62ad221d87ef682b5bdebe199e01f6"}, {file = "ruff-0.3.4-py3-none-win32.whl", hash = "sha256:af27ac187c0a331e8ef91d84bf1c3c6a5dea97e912a7560ac0cef25c526a4102"},
{file = "ruff-0.3.1-py3-none-win_amd64.whl", hash = "sha256:c0318a512edc9f4e010bbaab588b5294e78c5cdc9b02c3d8ab2d77c7ae1903e3"}, {file = "ruff-0.3.4-py3-none-win_amd64.whl", hash = "sha256:de0d5069b165e5a32b3c6ffbb81c350b1e3d3483347196ffdf86dc0ef9e37dd6"},
{file = "ruff-0.3.1-py3-none-win_arm64.whl", hash = "sha256:d3b60e44240f7e903e6dbae3139a65032ea4c6f2ad99b6265534ff1b83c20afa"}, {file = "ruff-0.3.4-py3-none-win_arm64.whl", hash = "sha256:6810563cc08ad0096b57c717bd78aeac888a1bfd38654d9113cb3dc4d3f74232"},
{file = "ruff-0.3.1.tar.gz", hash = "sha256:d30db97141fc2134299e6e983a6727922c9e03c031ae4883a6d69461de722ae7"}, {file = "ruff-0.3.4.tar.gz", hash = "sha256:f0f4484c6541a99862b693e13a151435a279b271cff20e37101116a21e2a1ad1"},
] ]
[[package]] [[package]]
name = "setuptools" name = "setuptools"
version = "69.1.1" version = "69.2.0"
description = "Easily download, build, install, upgrade, and uninstall Python packages" description = "Easily download, build, install, upgrade, and uninstall Python packages"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "setuptools-69.1.1-py3-none-any.whl", hash = "sha256:02fa291a0471b3a18b2b2481ed902af520c69e8ae0919c13da936542754b4c56"}, {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"},
{file = "setuptools-69.1.1.tar.gz", hash = "sha256:5c0806c7d9af348e6dd3777b4f4dbb42c7ad85b190104837488eab9a7c945cf8"}, {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"},
] ]
[package.extras] [package.extras]
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
[[package]] [[package]]
@@ -643,4 +660,4 @@ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.12" python-versions = "^3.12"
content-hash = "eab47ff7c7ac94755c2b2abd62783312b678d8825570d359c8419c54fbff822d" content-hash = "066efa29ad01e47892f68c061ebfc81eea6f8a541bb266ccef47011fdd4f5e89"

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "leggen" name = "leggen"
version = "0.3.0" version = "0.5.0"
description = "An Open Banking CLI" description = "An Open Banking CLI"
authors = ["Elisiário Couto <elisiario@couto.io>"] authors = ["Elisiário Couto <elisiario@couto.io>"]
readme = "README.md" readme = "README.md"
@@ -34,6 +34,7 @@ requests = "^2.31.0"
loguru = "^0.7.2" loguru = "^0.7.2"
tabulate = "^0.9.0" tabulate = "^0.9.0"
pymongo = "^4.6.1" pymongo = "^4.6.1"
discord-webhook = "^1.3.1"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
ruff = "^0.3.0" ruff = "^0.3.0"