mirror of
https://github.com/elisiariocouto/leggen.git
synced 2025-12-13 21:52:40 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
798a8f1880 | ||
|
|
7401ca62d2 | ||
|
|
e46634cf27 | ||
|
|
7b48bc080c | ||
|
|
0cb339366c | ||
|
|
3d36198b06 | ||
|
|
2352ea9e58 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -162,3 +162,4 @@ data/
|
||||
docker-compose.dev.yml
|
||||
nocodb/
|
||||
sql/
|
||||
leggen.db
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 24.2.0
|
||||
rev: 24.3.0
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python3.12
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: "v0.3.1"
|
||||
rev: "v0.3.4"
|
||||
hooks:
|
||||
- id: ruff
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
|
||||
25
CHANGELOG.md
25
CHANGELOG.md
@@ -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)
|
||||
|
||||
### Documentation
|
||||
|
||||
55
README.md
55
README.md
@@ -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 balances of 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
|
||||
- Schedule regular syncs with the database using Ofelia
|
||||
- Send notifications to Discord and/or Telegram when transactions match certain filters
|
||||
|
||||
## 🚀 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/.
|
||||
|
||||
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
|
||||
$ 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
|
||||
$ docker compose run leggen init
|
||||
```
|
||||
|
||||
Now you need to connect your bank accounts. Run the following command and follow the instructions:
|
||||
Run the following command and follow the instructions:
|
||||
|
||||
```bash
|
||||
$ docker compose run leggen bank add
|
||||
@@ -67,15 +98,17 @@ Usage: leggen [OPTIONS] COMMAND [ARGS]...
|
||||
Leggen: An Open Banking CLI
|
||||
|
||||
Options:
|
||||
--version Show the version and exit.
|
||||
-h, --help Show this message and exit.
|
||||
--version Show the version 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:
|
||||
bank Manage banks connections
|
||||
|
||||
Commands:
|
||||
balances List balances of all connected accounts
|
||||
init Create configuration file
|
||||
status List all connected banks and their status
|
||||
sync Sync all transactions with database
|
||||
transactions List transactions
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
version: '3.1'
|
||||
|
||||
services:
|
||||
# Defaults to `sync` command.
|
||||
leggen:
|
||||
image: elisiariocouto/leggen:latest
|
||||
command: sync
|
||||
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:
|
||||
- "./leggen:/root/.config/leggen"
|
||||
- "./leggen:/root/.config/leggen" # Default configuration file should be in this directory, named `config.toml`
|
||||
- "./db:/app"
|
||||
|
||||
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)
|
||||
@@ -1,107 +1,12 @@
|
||||
from datetime import datetime
|
||||
|
||||
import click
|
||||
|
||||
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.sqlite import save_transactions as save_transactions_sqlite
|
||||
from leggen.utils.notifications import send_notification
|
||||
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()
|
||||
@click.pass_context
|
||||
def sync(ctx: click.Context):
|
||||
@@ -118,6 +23,12 @@ def sync(ctx: click.Context):
|
||||
|
||||
for account in accounts:
|
||||
try:
|
||||
save_transactions(ctx, account)
|
||||
except Exception:
|
||||
error(f"[{account}] Error: Sync failed, skipping account.")
|
||||
new_transactions = save_transactions(ctx, account)
|
||||
except Exception as e:
|
||||
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
|
||||
|
||||
@@ -5,9 +5,9 @@ from pymongo.errors import DuplicateKeyError
|
||||
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
|
||||
mongo_uri = ctx.obj["mongo_uri"]
|
||||
mongo_uri = ctx.obj.get("database", {}).get("mongodb", {}).get("uri")
|
||||
client = MongoClient(mongo_uri)
|
||||
db = client["leggen"]
|
||||
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)
|
||||
|
||||
# Insert transactions into MongoDB
|
||||
new_transactions_count = 0
|
||||
duplicates_count = 0
|
||||
|
||||
new_transactions = []
|
||||
|
||||
for transaction in transactions:
|
||||
try:
|
||||
transactions_collection.insert_one(transaction)
|
||||
new_transactions_count += 1
|
||||
new_transactions.append(transaction)
|
||||
except DuplicateKeyError:
|
||||
# A transaction with the same ID already exists, skip insertion
|
||||
duplicates_count += 1
|
||||
|
||||
success(f"[{account}] Inserted {new_transactions_count} new transactions")
|
||||
success(f"[{account}] Inserted {len(new_transactions)} new transactions")
|
||||
if duplicates_count:
|
||||
warning(f"[{account}] Skipped {duplicates_count} duplicate transactions")
|
||||
|
||||
return new_transactions
|
||||
@@ -7,7 +7,7 @@ import click
|
||||
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
|
||||
|
||||
# Connect to SQLite database
|
||||
@@ -31,7 +31,6 @@ def save_transactions(ctx: click.Context, account: str, transactions: list):
|
||||
)
|
||||
|
||||
# Insert transactions into SQLite database
|
||||
new_transactions_count = 0
|
||||
duplicates_count = 0
|
||||
|
||||
# Prepare an SQL statement for inserting data
|
||||
@@ -48,6 +47,8 @@ def save_transactions(ctx: click.Context, account: str, transactions: list):
|
||||
rawTransaction
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"""
|
||||
|
||||
new_transactions = []
|
||||
|
||||
for transaction in transactions:
|
||||
try:
|
||||
cursor.execute(
|
||||
@@ -65,8 +66,7 @@ def save_transactions(ctx: click.Context, account: str, transactions: list):
|
||||
json.dumps(transaction["rawTransaction"]),
|
||||
),
|
||||
)
|
||||
|
||||
new_transactions_count += 1
|
||||
new_transactions.append(transaction)
|
||||
except IntegrityError:
|
||||
# A transaction with the same ID already exists, indicating a duplicate
|
||||
duplicates_count += 1
|
||||
@@ -75,6 +75,8 @@ def save_transactions(ctx: click.Context, account: str, transactions: list):
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
success(f"[{account}] Inserted {new_transactions_count} new transactions")
|
||||
success(f"[{account}] Inserted {len(new_transactions)} new transactions")
|
||||
if duplicates_count:
|
||||
warning(f"[{account}] Skipped {duplicates_count} duplicate transactions")
|
||||
|
||||
return new_transactions
|
||||
@@ -74,30 +74,33 @@ class Group(click.Group):
|
||||
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.pass_context
|
||||
def cli(ctx: click.Context):
|
||||
"""
|
||||
Leggen: An Open Banking CLI
|
||||
"""
|
||||
ctx.ensure_object(dict)
|
||||
|
||||
# Do not require authentication when printing help messages
|
||||
if "--help" in sys.argv[1:] or "-h" in sys.argv[1:]:
|
||||
return
|
||||
|
||||
# or when running the init command
|
||||
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"]
|
||||
token = get_token(ctx)
|
||||
ctx.obj["headers"] = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
33
leggen/notifications/discord.py
Normal file
33
leggen/notifications/discord.py
Normal 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
|
||||
43
leggen/notifications/telegram.py
Normal file
43
leggen/notifications/telegram.py
Normal 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
|
||||
@@ -7,13 +7,16 @@ import requests
|
||||
from leggen.utils.text import warning
|
||||
|
||||
|
||||
def create_token(config: dict) -> str:
|
||||
def create_token(ctx: click.Context) -> str:
|
||||
"""
|
||||
Create a new token
|
||||
"""
|
||||
res = requests.post(
|
||||
f"{config['api_url']}/token/new/",
|
||||
json={"secret_id": config["api_key"], "secret_key": config["api_secret"]},
|
||||
f"{ctx.obj['gocardless']['url']}/token/new/",
|
||||
json={
|
||||
"secret_id": ctx.obj["gocardless"]["key"],
|
||||
"secret_key": ctx.obj["gocardless"]["secret"],
|
||||
},
|
||||
)
|
||||
res.raise_for_status()
|
||||
auth = res.json()
|
||||
@@ -21,7 +24,7 @@ def create_token(config: dict) -> str:
|
||||
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
|
||||
"""
|
||||
@@ -30,10 +33,11 @@ def get_token(config: dict) -> str:
|
||||
with click.open_file(str(auth_file), "r") as f:
|
||||
auth = json.load(f)
|
||||
if not auth.get("access"):
|
||||
return create_token(config)
|
||||
return create_token(ctx)
|
||||
|
||||
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:
|
||||
res.raise_for_status()
|
||||
@@ -44,9 +48,9 @@ def get_token(config: dict) -> str:
|
||||
warning(
|
||||
f"Token probably expired, requesting a new one.\nResponse: {res.status_code}\n{res.text}"
|
||||
)
|
||||
return create_token(config)
|
||||
return create_token(ctx)
|
||||
else:
|
||||
return create_token(config)
|
||||
return create_token(ctx)
|
||||
|
||||
|
||||
def save_auth(d: dict):
|
||||
|
||||
@@ -1,29 +1,18 @@
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
import tomllib
|
||||
|
||||
from leggen.utils.text import error, info
|
||||
from leggen.utils.text import error
|
||||
|
||||
|
||||
def save_config(d: dict):
|
||||
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")
|
||||
def load_config(ctx: click.Context, _, filename):
|
||||
try:
|
||||
with click.open_file(str(config_file), "r") as f:
|
||||
config = json.load(f)
|
||||
return config
|
||||
with click.open_file(str(filename), "rb") as f:
|
||||
# TODO: Implement configuration file validation (use pydantic?)
|
||||
ctx.obj = tomllib.load(f)
|
||||
except FileNotFoundError:
|
||||
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)
|
||||
|
||||
112
leggen/utils/database.py
Normal file
112
leggen/utils/database.py
Normal 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)
|
||||
@@ -9,7 +9,7 @@ def get(ctx: click.Context, path: str, params: dict = {}):
|
||||
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)
|
||||
try:
|
||||
res.raise_for_status()
|
||||
@@ -24,7 +24,7 @@ def post(ctx: click.Context, path: str, data: dict = {}):
|
||||
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)
|
||||
try:
|
||||
res.raise_for_status()
|
||||
@@ -39,7 +39,7 @@ def put(ctx: click.Context, path: str, data: dict = {}):
|
||||
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)
|
||||
try:
|
||||
res.raise_for_status()
|
||||
@@ -54,7 +54,7 @@ def delete(ctx: click.Context, path: str):
|
||||
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"])
|
||||
try:
|
||||
res.raise_for_status()
|
||||
|
||||
46
leggen/utils/notifications.py
Normal file
46
leggen/utils/notifications.py
Normal 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
131
poetry.lock
generated
@@ -2,33 +2,33 @@
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "24.2.0"
|
||||
version = "24.3.0"
|
||||
description = "The uncompromising code formatter."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "black-24.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6981eae48b3b33399c8757036c7f5d48a535b962a7c2310d19361edeef64ce29"},
|
||||
{file = "black-24.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d533d5e3259720fdbc1b37444491b024003e012c5173f7d06825a77508085430"},
|
||||
{file = "black-24.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61a0391772490ddfb8a693c067df1ef5227257e72b0e4108482b8d41b5aee13f"},
|
||||
{file = "black-24.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:992e451b04667116680cb88f63449267c13e1ad134f30087dec8527242e9862a"},
|
||||
{file = "black-24.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd"},
|
||||
{file = "black-24.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2"},
|
||||
{file = "black-24.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92"},
|
||||
{file = "black-24.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23"},
|
||||
{file = "black-24.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b"},
|
||||
{file = "black-24.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9"},
|
||||
{file = "black-24.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693"},
|
||||
{file = "black-24.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982"},
|
||||
{file = "black-24.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e53a8c630f71db01b28cd9602a1ada68c937cbf2c333e6ed041390d6968faf4"},
|
||||
{file = "black-24.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:93601c2deb321b4bad8f95df408e3fb3943d85012dddb6121336b8e24a0d1218"},
|
||||
{file = "black-24.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0057f800de6acc4407fe75bb147b0c2b5cbb7c3ed110d3e5999cd01184d53b0"},
|
||||
{file = "black-24.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:faf2ee02e6612577ba0181f4347bcbcf591eb122f7841ae5ba233d12c39dcb4d"},
|
||||
{file = "black-24.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8"},
|
||||
{file = "black-24.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8"},
|
||||
{file = "black-24.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540"},
|
||||
{file = "black-24.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31"},
|
||||
{file = "black-24.2.0-py3-none-any.whl", hash = "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6"},
|
||||
{file = "black-24.2.0.tar.gz", hash = "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894"},
|
||||
{file = "black-24.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395"},
|
||||
{file = "black-24.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995"},
|
||||
{file = "black-24.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7"},
|
||||
{file = "black-24.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0"},
|
||||
{file = "black-24.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9"},
|
||||
{file = "black-24.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597"},
|
||||
{file = "black-24.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d"},
|
||||
{file = "black-24.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5"},
|
||||
{file = "black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f"},
|
||||
{file = "black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11"},
|
||||
{file = "black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4"},
|
||||
{file = "black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5"},
|
||||
{file = "black-24.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837"},
|
||||
{file = "black-24.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd"},
|
||||
{file = "black-24.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213"},
|
||||
{file = "black-24.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959"},
|
||||
{file = "black-24.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb"},
|
||||
{file = "black-24.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7"},
|
||||
{file = "black-24.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7"},
|
||||
{file = "black-24.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f"},
|
||||
{file = "black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93"},
|
||||
{file = "black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -190,6 +190,23 @@ files = [
|
||||
{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]]
|
||||
name = "distlib"
|
||||
version = "0.3.8"
|
||||
@@ -223,18 +240,18 @@ wmi = ["wmi (>=1.5.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "3.13.1"
|
||||
version = "3.13.3"
|
||||
description = "A platform independent file lock."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"},
|
||||
{file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"},
|
||||
{file = "filelock-3.13.3-py3-none-any.whl", hash = "sha256:5ffa845303983e7a0b7ae17636509bc97997d58afeafa72fb141a17b152284cb"},
|
||||
{file = "filelock-3.13.3.tar.gz", hash = "sha256:a79895a25bbefdf55d1a2a0a80968f7dbb28edcd6d4234a0afb3f37ecde4b546"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"]
|
||||
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)"]
|
||||
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.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"]
|
||||
typing = ["typing-extensions (>=4.8)"]
|
||||
|
||||
[[package]]
|
||||
@@ -307,13 +324,13 @@ setuptools = "*"
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "23.2"
|
||||
version = "24.0"
|
||||
description = "Core utilities for Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
|
||||
{file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
|
||||
{file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"},
|
||||
{file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -344,13 +361,13 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-
|
||||
|
||||
[[package]]
|
||||
name = "pre-commit"
|
||||
version = "3.6.2"
|
||||
version = "3.7.0"
|
||||
description = "A framework for managing and maintaining multi-language pre-commit hooks."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "pre_commit-3.6.2-py2.py3-none-any.whl", hash = "sha256:ba637c2d7a670c10daedc059f5c49b5bd0aadbccfcd7ec15592cf9665117532c"},
|
||||
{file = "pre_commit-3.6.2.tar.gz", hash = "sha256:c3ef34f463045c88658c5b99f38c1e297abdcc0ff13f98d3370055fbbfabc67e"},
|
||||
{file = "pre_commit-3.7.0-py2.py3-none-any.whl", hash = "sha256:5eae9e10c2b5ac51577c3452ec0a490455c45a0533f7960f993a0d01e59decab"},
|
||||
{file = "pre_commit-3.7.0.tar.gz", hash = "sha256:e209d61b8acdcf742404408531f0c37d49d2c734fd7cff2d6076083d191cb060"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -535,44 +552,44 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.3.1"
|
||||
version = "0.3.4"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
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.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ae7954c8f692b70e6a206087ae3988acc9295d84c550f8d90b66c62424c16771"},
|
||||
{file = "ruff-0.3.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b730f56ccf91225da0f06cfe421e83b8cc27b2a79393db9c3df02ed7e2bbc01"},
|
||||
{file = "ruff-0.3.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c78bfa85637668f47bd82aa2ae17de2b34221ac23fea30926f6409f9e37fc927"},
|
||||
{file = "ruff-0.3.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6abaad602d6e6daaec444cbf4d9364df0a783e49604c21499f75bb92237d4af"},
|
||||
{file = "ruff-0.3.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5f0c21b6914c3c9a25a59497cbb1e5b6c2d8d9beecc9b8e03ee986e24eee072e"},
|
||||
{file = "ruff-0.3.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:434c3fc72e6311c85cd143c4c448b0e60e025a9ac1781e63ba222579a8c29200"},
|
||||
{file = "ruff-0.3.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78a7025e6312cbba496341da5062e7cdd47d95f45c1b903e635cdeb1ba5ec2b9"},
|
||||
{file = "ruff-0.3.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52b02bb46f1a79b0c1fa93f6495bc7e77e4ef76e6c28995b4974a20ed09c0833"},
|
||||
{file = "ruff-0.3.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:11b5699c42f7d0b771c633d620f2cb22e727fb226273aba775a91784a9ed856c"},
|
||||
{file = "ruff-0.3.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:54e5dca3e411772b51194b3102b5f23b36961e8ede463776b289b78180df71a0"},
|
||||
{file = "ruff-0.3.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:951efb610c5844e668bbec4f71cf704f8645cf3106e13f283413969527ebfded"},
|
||||
{file = "ruff-0.3.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:09c7333b25e983aabcf6e38445252cff0b4745420fc3bda45b8fce791cc7e9ce"},
|
||||
{file = "ruff-0.3.1-py3-none-win32.whl", hash = "sha256:d937f9b99ebf346e0606c3faf43c1e297a62ad221d87ef682b5bdebe199e01f6"},
|
||||
{file = "ruff-0.3.1-py3-none-win_amd64.whl", hash = "sha256:c0318a512edc9f4e010bbaab588b5294e78c5cdc9b02c3d8ab2d77c7ae1903e3"},
|
||||
{file = "ruff-0.3.1-py3-none-win_arm64.whl", hash = "sha256:d3b60e44240f7e903e6dbae3139a65032ea4c6f2ad99b6265534ff1b83c20afa"},
|
||||
{file = "ruff-0.3.1.tar.gz", hash = "sha256:d30db97141fc2134299e6e983a6727922c9e03c031ae4883a6d69461de722ae7"},
|
||||
{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.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6fc14fa742e1d8f24910e1fff0bd5e26d395b0e0e04cc1b15c7c5e5fe5b4af91"},
|
||||
{file = "ruff-0.3.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3ee7880f653cc03749a3bfea720cf2a192e4f884925b0cf7eecce82f0ce5854"},
|
||||
{file = "ruff-0.3.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf133dd744f2470b347f602452a88e70dadfbe0fcfb5fd46e093d55da65f82f7"},
|
||||
{file = "ruff-0.3.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f3860057590e810c7ffea75669bdc6927bfd91e29b4baa9258fd48b540a4365"},
|
||||
{file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:986f2377f7cf12efac1f515fc1a5b753c000ed1e0a6de96747cdf2da20a1b369"},
|
||||
{file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fd98e85869603e65f554fdc5cddf0712e352fe6e61d29d5a6fe087ec82b76c"},
|
||||
{file = "ruff-0.3.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64abeed785dad51801b423fa51840b1764b35d6c461ea8caef9cf9e5e5ab34d9"},
|
||||
{file = "ruff-0.3.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df52972138318bc7546d92348a1ee58449bc3f9eaf0db278906eb511889c4b50"},
|
||||
{file = "ruff-0.3.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:98e98300056445ba2cc27d0b325fd044dc17fcc38e4e4d2c7711585bd0a958ed"},
|
||||
{file = "ruff-0.3.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:519cf6a0ebed244dce1dc8aecd3dc99add7a2ee15bb68cf19588bb5bf58e0488"},
|
||||
{file = "ruff-0.3.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bb0acfb921030d00070539c038cd24bb1df73a2981e9f55942514af8b17be94e"},
|
||||
{file = "ruff-0.3.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cf187a7e7098233d0d0c71175375c5162f880126c4c716fa28a8ac418dcf3378"},
|
||||
{file = "ruff-0.3.4-py3-none-win32.whl", hash = "sha256:af27ac187c0a331e8ef91d84bf1c3c6a5dea97e912a7560ac0cef25c526a4102"},
|
||||
{file = "ruff-0.3.4-py3-none-win_amd64.whl", hash = "sha256:de0d5069b165e5a32b3c6ffbb81c350b1e3d3483347196ffdf86dc0ef9e37dd6"},
|
||||
{file = "ruff-0.3.4-py3-none-win_arm64.whl", hash = "sha256:6810563cc08ad0096b57c717bd78aeac888a1bfd38654d9113cb3dc4d3f74232"},
|
||||
{file = "ruff-0.3.4.tar.gz", hash = "sha256:f0f4484c6541a99862b693e13a151435a279b271cff20e37101116a21e2a1ad1"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "setuptools"
|
||||
version = "69.1.1"
|
||||
version = "69.2.0"
|
||||
description = "Easily download, build, install, upgrade, and uninstall Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "setuptools-69.1.1-py3-none-any.whl", hash = "sha256:02fa291a0471b3a18b2b2481ed902af520c69e8ae0919c13da936542754b4c56"},
|
||||
{file = "setuptools-69.1.1.tar.gz", hash = "sha256:5c0806c7d9af348e6dd3777b4f4dbb42c7ad85b190104837488eab9a7c945cf8"},
|
||||
{file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"},
|
||||
{file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"},
|
||||
]
|
||||
|
||||
[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"]
|
||||
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"]
|
||||
|
||||
[[package]]
|
||||
@@ -643,4 +660,4 @@ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.12"
|
||||
content-hash = "eab47ff7c7ac94755c2b2abd62783312b678d8825570d359c8419c54fbff822d"
|
||||
content-hash = "066efa29ad01e47892f68c061ebfc81eea6f8a541bb266ccef47011fdd4f5e89"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "leggen"
|
||||
version = "0.3.0"
|
||||
version = "0.5.0"
|
||||
description = "An Open Banking CLI"
|
||||
authors = ["Elisiário Couto <elisiario@couto.io>"]
|
||||
readme = "README.md"
|
||||
@@ -34,6 +34,7 @@ requests = "^2.31.0"
|
||||
loguru = "^0.7.2"
|
||||
tabulate = "^0.9.0"
|
||||
pymongo = "^4.6.1"
|
||||
discord-webhook = "^1.3.1"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
ruff = "^0.3.0"
|
||||
|
||||
Reference in New Issue
Block a user