mirror of
https://github.com/elisiariocouto/leggen.git
synced 2025-12-13 19:32:25 +00:00
feat(notifications): Add support for transaction filter and notifications via Discord.
This commit is contained in:
committed by
Elisiário Couto
parent
3d36198b06
commit
0cb339366c
46
README.md
46
README.md
@@ -29,24 +29,48 @@ Having your bank data in a database, gives you the power to backup, analyze and
|
|||||||
- Sync all transactions with a SQLite or MongoDB database
|
- Sync all transactions with a SQLite 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 Discrod 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](docker-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
|
||||||
|
|
||||||
|
[notifications.discord]
|
||||||
|
webhook = "https://discord.com/api/webhooks/..."
|
||||||
|
|
||||||
|
[filters]
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
[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 +91,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
|
||||||
|
|||||||
@@ -6,13 +6,8 @@ services:
|
|||||||
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:
|
||||||
|
|||||||
@@ -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)
|
|
||||||
@@ -92,7 +92,7 @@ def save_transactions(ctx: click.Context, account: str):
|
|||||||
}
|
}
|
||||||
transactions.append(t)
|
transactions.append(t)
|
||||||
|
|
||||||
sqlite = ctx.obj["sqlite"]
|
sqlite = ctx.obj.get("database", {}).get("sqlite", True)
|
||||||
info(
|
info(
|
||||||
f"[{account}] Fetched {len(transactions)} transactions, saving to {'SQLite' if sqlite else 'MongoDB'}"
|
f"[{account}] Fetched {len(transactions)} transactions, saving to {'SQLite' if sqlite else 'MongoDB'}"
|
||||||
)
|
)
|
||||||
@@ -119,5 +119,5 @@ def sync(ctx: click.Context):
|
|||||||
for account in accounts:
|
for account in accounts:
|
||||||
try:
|
try:
|
||||||
save_transactions(ctx, account)
|
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}")
|
||||||
|
|||||||
@@ -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}"}
|
||||||
|
|||||||
30
leggen/notifications/discord.py
Normal file
30
leggen/notifications/discord.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
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()
|
||||||
|
response.raise_for_status()
|
||||||
@@ -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):
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ import click
|
|||||||
from pymongo import MongoClient
|
from pymongo import MongoClient
|
||||||
from pymongo.errors import DuplicateKeyError
|
from pymongo.errors import DuplicateKeyError
|
||||||
|
|
||||||
|
from leggen.notifications.discord import send_message
|
||||||
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 save_transactions(ctx: click.Context, account: str, transactions: 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"]
|
||||||
@@ -19,14 +20,38 @@ def save_transactions(ctx: click.Context, account: str, transactions: list):
|
|||||||
new_transactions_count = 0
|
new_transactions_count = 0
|
||||||
duplicates_count = 0
|
duplicates_count = 0
|
||||||
|
|
||||||
|
notification_transactions = []
|
||||||
|
filters_case_insensitive = {}
|
||||||
|
if ctx.obj.get("filters", {}).get("enabled", False):
|
||||||
|
filters_case_insensitive = ctx.obj.get("filters", {}).get(
|
||||||
|
"case-insensitive", {}
|
||||||
|
)
|
||||||
|
|
||||||
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_count += 1
|
||||||
|
|
||||||
|
# Add transaction to the list of transactions to be sent as a notification
|
||||||
|
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"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
# Send a notification with the transactions that match the filters
|
||||||
|
if notification_transactions:
|
||||||
|
send_message(ctx, notification_transactions)
|
||||||
|
|
||||||
success(f"[{account}] Inserted {new_transactions_count} new transactions")
|
success(f"[{account}] Inserted {new_transactions_count} new transactions")
|
||||||
if duplicates_count:
|
if duplicates_count:
|
||||||
warning(f"[{account}] Skipped {duplicates_count} duplicate transactions")
|
warning(f"[{account}] Skipped {duplicates_count} duplicate transactions")
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from sqlite3 import IntegrityError
|
|||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
|
from leggen.notifications.discord import send_message
|
||||||
from leggen.utils.text import success, warning
|
from leggen.utils.text import success, warning
|
||||||
|
|
||||||
|
|
||||||
@@ -48,6 +49,13 @@ def save_transactions(ctx: click.Context, account: str, transactions: list):
|
|||||||
rawTransaction
|
rawTransaction
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"""
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"""
|
||||||
|
|
||||||
|
notification_transactions = []
|
||||||
|
filters_case_insensitive = {}
|
||||||
|
if ctx.obj.get("filters", {}).get("enabled", False):
|
||||||
|
filters_case_insensitive = ctx.obj.get("filters", {}).get(
|
||||||
|
"case-insensitive", {}
|
||||||
|
)
|
||||||
|
|
||||||
for transaction in transactions:
|
for transaction in transactions:
|
||||||
try:
|
try:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
@@ -65,8 +73,19 @@ def save_transactions(ctx: click.Context, account: str, transactions: list):
|
|||||||
json.dumps(transaction["rawTransaction"]),
|
json.dumps(transaction["rawTransaction"]),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
new_transactions_count += 1
|
new_transactions_count += 1
|
||||||
|
|
||||||
|
# Add transaction to the list of transactions to be sent as a notification
|
||||||
|
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"],
|
||||||
|
}
|
||||||
|
)
|
||||||
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 +94,10 @@ def save_transactions(ctx: click.Context, account: str, transactions: list):
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
# Send a notification with the transactions that match the filters
|
||||||
|
if notification_transactions:
|
||||||
|
send_message(ctx, notification_transactions)
|
||||||
|
|
||||||
success(f"[{account}] Inserted {new_transactions_count} new transactions")
|
success(f"[{account}] Inserted {new_transactions_count} new transactions")
|
||||||
if duplicates_count:
|
if duplicates_count:
|
||||||
warning(f"[{account}] Skipped {duplicates_count} duplicate transactions")
|
warning(f"[{account}] Skipped {duplicates_count} duplicate transactions")
|
||||||
|
|||||||
19
poetry.lock
generated
19
poetry.lock
generated
@@ -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"
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user